package main import ( "context" "encoding/json" "fmt" "io/ioutil" "math/big" "math/rand" "os" "path/filepath" "time" saproof "github.com/filecoin-project/specs-actors/actors/runtime/proof" "github.com/docker/go-units" logging "github.com/ipfs/go-log/v2" "github.com/minio/blake2b-simd" "github.com/mitchellh/go-homedir" "github.com/urfave/cli/v2" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" paramfetch "github.com/filecoin-project/go-paramfetch" "github.com/filecoin-project/go-state-types/abi" lcli "github.com/filecoin-project/lotus/cli" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper/basicfs" "github.com/filecoin-project/lotus/extern/sector-storage/stores" "github.com/filecoin-project/specs-actors/actors/builtin/miner" "github.com/filecoin-project/specs-storage/storage" lapi "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/genesis" ) var log = logging.Logger("lotus-bench") type BenchResults struct { SectorSize abi.SectorSize SealingResults []SealingResult PostGenerateCandidates time.Duration PostWinningProofCold time.Duration PostWinningProofHot time.Duration VerifyWinningPostCold time.Duration VerifyWinningPostHot time.Duration PostWindowProofCold time.Duration PostWindowProofHot time.Duration VerifyWindowPostCold time.Duration VerifyWindowPostHot time.Duration } type SealingResult struct { AddPiece time.Duration PreCommit1 time.Duration PreCommit2 time.Duration Commit1 time.Duration Commit2 time.Duration Verify time.Duration Unseal time.Duration } type Commit2In struct { SectorNum int64 Phase1Out []byte SectorSize uint64 } func main() { logging.SetLogLevel("*", "INFO") log.Info("Starting lotus-bench") miner.SupportedProofTypes[abi.RegisteredSealProof_StackedDrg2KiBV1] = struct{}{} app := &cli.App{ Name: "lotus-bench", Usage: "Benchmark performance of lotus on your hardware", Version: build.UserVersion(), Commands: []*cli.Command{ proveCmd, sealBenchCmd, importBenchCmd, }, } if err := app.Run(os.Args); err != nil { log.Warnf("%+v", err) return } } var sealBenchCmd = &cli.Command{ Name: "sealing", Flags: []cli.Flag{ &cli.StringFlag{ Name: "storage-dir", Value: "~/.lotus-bench", Usage: "Path to the storage directory that will store sectors long term", }, &cli.StringFlag{ Name: "sector-size", Value: "512MiB", Usage: "size of the sectors in bytes, i.e. 32GiB", }, &cli.BoolFlag{ Name: "no-gpu", Usage: "disable gpu usage for the benchmark run", }, &cli.StringFlag{ Name: "miner-addr", Usage: "pass miner address (only necessary if using existing sectorbuilder)", Value: "t01000", }, &cli.StringFlag{ Name: "benchmark-existing-sectorbuilder", Usage: "pass a directory to run post timings on an existing sectorbuilder", }, &cli.BoolFlag{ Name: "json-out", Usage: "output results in json format", }, &cli.BoolFlag{ Name: "skip-commit2", Usage: "skip the commit2 (snark) portion of the benchmark", }, &cli.BoolFlag{ Name: "skip-unseal", Usage: "skip the unseal portion of the benchmark", }, &cli.StringFlag{ Name: "save-commit2-input", Usage: "Save commit2 input to a file", }, &cli.IntFlag{ Name: "num-sectors", Value: 1, }, &cli.IntFlag{ Name: "parallel", Value: 1, }, }, Action: func(c *cli.Context) error { if c.Bool("no-gpu") { err := os.Setenv("BELLMAN_NO_GPU", "1") if err != nil { return xerrors.Errorf("setting no-gpu flag: %w", err) } } robench := c.String("benchmark-existing-sectorbuilder") var sbdir string if robench == "" { sdir, err := homedir.Expand(c.String("storage-dir")) if err != nil { return err } err = os.MkdirAll(sdir, 0775) //nolint:gosec if err != nil { return xerrors.Errorf("creating sectorbuilder dir: %w", err) } tsdir, err := ioutil.TempDir(sdir, "bench") if err != nil { return err } defer func() { if err := os.RemoveAll(tsdir); err != nil { log.Warn("remove all: ", err) } }() // TODO: pretty sure this isnt even needed? if err := os.MkdirAll(tsdir, 0775); err != nil { return err } sbdir = tsdir } else { exp, err := homedir.Expand(robench) if err != nil { return err } sbdir = exp } // miner address maddr, err := address.NewFromString(c.String("miner-addr")) if err != nil { return err } amid, err := address.IDFromAddress(maddr) if err != nil { return err } mid := abi.ActorID(amid) // sector size sectorSizeInt, err := units.RAMInBytes(c.String("sector-size")) if err != nil { return err } sectorSize := abi.SectorSize(sectorSizeInt) spt, err := ffiwrapper.SealProofTypeFromSectorSize(sectorSize) if err != nil { return err } cfg := &ffiwrapper.Config{ SealProofType: spt, } // Only fetch parameters if actually needed if !c.Bool("skip-commit2") { if err := paramfetch.GetParams(lcli.ReqContext(c), build.ParametersJSON(), uint64(sectorSize)); err != nil { return xerrors.Errorf("getting params: %w", err) } } sbfs := &basicfs.Provider{ Root: sbdir, } sb, err := ffiwrapper.New(sbfs, cfg) if err != nil { return err } var sealTimings []SealingResult var sealedSectors []saproof.SectorInfo if robench == "" { var err error parCfg := ParCfg{ PreCommit1: c.Int("parallel"), PreCommit2: 1, Commit: 1, } sealTimings, sealedSectors, err = runSeals(sb, sbfs, c.Int("num-sectors"), parCfg, mid, sectorSize, []byte(c.String("ticket-preimage")), c.String("save-commit2-input"), c.Bool("skip-commit2"), c.Bool("skip-unseal")) if err != nil { return xerrors.Errorf("failed to run seals: %w", err) } } beforePost := time.Now() var challenge [32]byte rand.Read(challenge[:]) if robench != "" { // TODO: implement sbfs.List() and use that for all cases (preexisting sectorbuilder or not) // TODO: this assumes we only ever benchmark a preseal // sectorbuilder directory... we need a better way to handle // this in other cases fdata, err := ioutil.ReadFile(filepath.Join(sbdir, "pre-seal-"+maddr.String()+".json")) if err != nil { return err } var genmm map[string]genesis.Miner if err := json.Unmarshal(fdata, &genmm); err != nil { return err } genm, ok := genmm[maddr.String()] if !ok { return xerrors.Errorf("preseal file didnt have expected miner in it") } for _, s := range genm.Sectors { sealedSectors = append(sealedSectors, saproof.SectorInfo{ SealedCID: s.CommR, SectorNumber: s.SectorID, SealProof: s.ProofType, }) } } bo := BenchResults{ SectorSize: sectorSize, SealingResults: sealTimings, } if !c.Bool("skip-commit2") { log.Info("generating winning post candidates") wipt, err := spt.RegisteredWinningPoStProof() if err != nil { return err } fcandidates, err := ffiwrapper.ProofVerifier.GenerateWinningPoStSectorChallenge(context.TODO(), wipt, mid, challenge[:], uint64(len(sealedSectors))) if err != nil { return err } candidates := make([]saproof.SectorInfo, len(fcandidates)) for i, fcandidate := range fcandidates { candidates[i] = sealedSectors[fcandidate] } gencandidates := time.Now() log.Info("computing winning post snark (cold)") proof1, err := sb.GenerateWinningPoSt(context.TODO(), mid, candidates, challenge[:]) if err != nil { return err } winningpost1 := time.Now() log.Info("computing winning post snark (hot)") proof2, err := sb.GenerateWinningPoSt(context.TODO(), mid, candidates, challenge[:]) if err != nil { return err } winnningpost2 := time.Now() pvi1 := saproof.WinningPoStVerifyInfo{ Randomness: abi.PoStRandomness(challenge[:]), Proofs: proof1, ChallengedSectors: candidates, Prover: mid, } ok, err := ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi1) if err != nil { return err } if !ok { log.Error("post verification failed") } verifyWinningPost1 := time.Now() pvi2 := saproof.WinningPoStVerifyInfo{ Randomness: abi.PoStRandomness(challenge[:]), Proofs: proof2, ChallengedSectors: candidates, Prover: mid, } ok, err = ffiwrapper.ProofVerifier.VerifyWinningPoSt(context.TODO(), pvi2) if err != nil { return err } if !ok { log.Error("post verification failed") } verifyWinningPost2 := time.Now() log.Info("computing window post snark (cold)") wproof1, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, sealedSectors, challenge[:]) if err != nil { return err } windowpost1 := time.Now() log.Info("computing window post snark (hot)") wproof2, _, err := sb.GenerateWindowPoSt(context.TODO(), mid, sealedSectors, challenge[:]) if err != nil { return err } windowpost2 := time.Now() wpvi1 := saproof.WindowPoStVerifyInfo{ Randomness: challenge[:], Proofs: wproof1, ChallengedSectors: sealedSectors, Prover: mid, } ok, err = ffiwrapper.ProofVerifier.VerifyWindowPoSt(context.TODO(), wpvi1) if err != nil { return err } if !ok { log.Error("post verification failed") } verifyWindowpost1 := time.Now() wpvi2 := saproof.WindowPoStVerifyInfo{ Randomness: challenge[:], Proofs: wproof2, ChallengedSectors: sealedSectors, Prover: mid, } ok, err = ffiwrapper.ProofVerifier.VerifyWindowPoSt(context.TODO(), wpvi2) if err != nil { return err } if !ok { log.Error("post verification failed") } verifyWindowpost2 := time.Now() bo.PostGenerateCandidates = gencandidates.Sub(beforePost) bo.PostWinningProofCold = winningpost1.Sub(gencandidates) bo.PostWinningProofHot = winnningpost2.Sub(winningpost1) bo.VerifyWinningPostCold = verifyWinningPost1.Sub(winnningpost2) bo.VerifyWinningPostHot = verifyWinningPost2.Sub(verifyWinningPost1) bo.PostWindowProofCold = windowpost1.Sub(verifyWinningPost2) bo.PostWindowProofHot = windowpost2.Sub(windowpost1) bo.VerifyWindowPostCold = verifyWindowpost1.Sub(windowpost2) bo.VerifyWindowPostHot = verifyWindowpost2.Sub(verifyWindowpost1) } if c.Bool("json-out") { data, err := json.MarshalIndent(bo, "", " ") if err != nil { return err } fmt.Println(string(data)) } else { fmt.Printf("----\nresults (v27) (%d)\n", sectorSize) if robench == "" { fmt.Printf("seal: addPiece: %s (%s)\n", bo.SealingResults[0].AddPiece, bps(bo.SectorSize, bo.SealingResults[0].AddPiece)) // TODO: average across multiple sealings fmt.Printf("seal: preCommit phase 1: %s (%s)\n", bo.SealingResults[0].PreCommit1, bps(bo.SectorSize, bo.SealingResults[0].PreCommit1)) fmt.Printf("seal: preCommit phase 2: %s (%s)\n", bo.SealingResults[0].PreCommit2, bps(bo.SectorSize, bo.SealingResults[0].PreCommit2)) fmt.Printf("seal: commit phase 1: %s (%s)\n", bo.SealingResults[0].Commit1, bps(bo.SectorSize, bo.SealingResults[0].Commit1)) fmt.Printf("seal: commit phase 2: %s (%s)\n", bo.SealingResults[0].Commit2, bps(bo.SectorSize, bo.SealingResults[0].Commit2)) fmt.Printf("seal: verify: %s\n", bo.SealingResults[0].Verify) if !c.Bool("skip-unseal") { fmt.Printf("unseal: %s (%s)\n", bo.SealingResults[0].Unseal, bps(bo.SectorSize, bo.SealingResults[0].Unseal)) } fmt.Println("") } if !c.Bool("skip-commit2") { fmt.Printf("generate candidates: %s (%s)\n", bo.PostGenerateCandidates, bps(bo.SectorSize*abi.SectorSize(len(bo.SealingResults)), bo.PostGenerateCandidates)) fmt.Printf("compute winning post proof (cold): %s\n", bo.PostWinningProofCold) fmt.Printf("compute winning post proof (hot): %s\n", bo.PostWinningProofHot) fmt.Printf("verify winning post proof (cold): %s\n", bo.VerifyWinningPostCold) fmt.Printf("verify winning post proof (hot): %s\n\n", bo.VerifyWinningPostHot) fmt.Printf("compute window post proof (cold): %s\n", bo.PostWindowProofCold) fmt.Printf("compute window post proof (hot): %s\n", bo.PostWindowProofHot) fmt.Printf("verify window post proof (cold): %s\n", bo.VerifyWindowPostCold) fmt.Printf("verify window post proof (hot): %s\n", bo.VerifyWindowPostHot) } } return nil }, } type ParCfg struct { PreCommit1 int PreCommit2 int Commit int } func runSeals(sb *ffiwrapper.Sealer, sbfs *basicfs.Provider, numSectors int, par ParCfg, mid abi.ActorID, sectorSize abi.SectorSize, ticketPreimage []byte, saveC2inp string, skipc2, skipunseal bool) ([]SealingResult, []saproof.SectorInfo, error) { var pieces []abi.PieceInfo sealTimings := make([]SealingResult, numSectors) sealedSectors := make([]saproof.SectorInfo, numSectors) preCommit2Sema := make(chan struct{}, par.PreCommit2) commitSema := make(chan struct{}, par.Commit) if numSectors%par.PreCommit1 != 0 { return nil, nil, fmt.Errorf("parallelism factor must cleanly divide numSectors") } for i := abi.SectorNumber(1); i <= abi.SectorNumber(numSectors); i++ { sid := abi.SectorID{ Miner: mid, Number: i, } start := time.Now() log.Infof("[%d] Writing piece into sector...", i) r := rand.New(rand.NewSource(100 + int64(i))) pi, err := sb.AddPiece(context.TODO(), sid, nil, abi.PaddedPieceSize(sectorSize).Unpadded(), r) if err != nil { return nil, nil, err } pieces = append(pieces, pi) sealTimings[i-1].AddPiece = time.Since(start) } sectorsPerWorker := numSectors / par.PreCommit1 errs := make(chan error, par.PreCommit1) for wid := 0; wid < par.PreCommit1; wid++ { go func(worker int) { sealerr := func() error { start := 1 + (worker * sectorsPerWorker) end := start + sectorsPerWorker for i := abi.SectorNumber(start); i < abi.SectorNumber(end); i++ { ix := int(i - 1) sid := abi.SectorID{ Miner: mid, Number: i, } start := time.Now() trand := blake2b.Sum256(ticketPreimage) ticket := abi.SealRandomness(trand[:]) log.Infof("[%d] Running replication(1)...", i) pieces := []abi.PieceInfo{pieces[ix]} pc1o, err := sb.SealPreCommit1(context.TODO(), sid, ticket, pieces) if err != nil { return xerrors.Errorf("commit: %w", err) } precommit1 := time.Now() preCommit2Sema <- struct{}{} pc2Start := time.Now() log.Infof("[%d] Running replication(2)...", i) cids, err := sb.SealPreCommit2(context.TODO(), sid, pc1o) if err != nil { return xerrors.Errorf("commit: %w", err) } precommit2 := time.Now() <-preCommit2Sema sealedSectors[ix] = saproof.SectorInfo{ SealProof: sb.SealProofType(), SectorNumber: i, SealedCID: cids.Sealed, } seed := lapi.SealSeed{ Epoch: 101, Value: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 255}, } commitSema <- struct{}{} commitStart := time.Now() log.Infof("[%d] Generating PoRep for sector (1)", i) c1o, err := sb.SealCommit1(context.TODO(), sid, ticket, seed.Value, pieces, cids) if err != nil { return err } sealcommit1 := time.Now() log.Infof("[%d] Generating PoRep for sector (2)", i) if saveC2inp != "" { c2in := Commit2In{ SectorNum: int64(i), Phase1Out: c1o, SectorSize: uint64(sectorSize), } b, err := json.Marshal(&c2in) if err != nil { return err } if err := ioutil.WriteFile(saveC2inp, b, 0664); err != nil { log.Warnf("%+v", err) } } var proof storage.Proof if !skipc2 { proof, err = sb.SealCommit2(context.TODO(), sid, c1o) if err != nil { return err } } sealcommit2 := time.Now() <-commitSema if !skipc2 { svi := saproof.SealVerifyInfo{ SectorID: abi.SectorID{Miner: mid, Number: i}, SealedCID: cids.Sealed, SealProof: sb.SealProofType(), Proof: proof, DealIDs: nil, Randomness: ticket, InteractiveRandomness: seed.Value, UnsealedCID: cids.Unsealed, } ok, err := ffiwrapper.ProofVerifier.VerifySeal(svi) if err != nil { return err } if !ok { return xerrors.Errorf("porep proof for sector %d was invalid", i) } } verifySeal := time.Now() if !skipunseal { log.Infof("[%d] Unsealing sector", i) { p, done, err := sbfs.AcquireSector(context.TODO(), abi.SectorID{Miner: mid, Number: 1}, stores.FTUnsealed, stores.FTNone, stores.PathSealing) if err != nil { return xerrors.Errorf("acquire unsealed sector for removing: %w", err) } done() if err := os.Remove(p.Unsealed); err != nil { return xerrors.Errorf("removing unsealed sector: %w", err) } } err := sb.UnsealPiece(context.TODO(), abi.SectorID{Miner: mid, Number: 1}, 0, abi.PaddedPieceSize(sectorSize).Unpadded(), ticket, cids.Unsealed) if err != nil { return err } } unseal := time.Now() sealTimings[ix].PreCommit1 = precommit1.Sub(start) sealTimings[ix].PreCommit2 = precommit2.Sub(pc2Start) sealTimings[ix].Commit1 = sealcommit1.Sub(commitStart) sealTimings[ix].Commit2 = sealcommit2.Sub(sealcommit1) sealTimings[ix].Verify = verifySeal.Sub(sealcommit2) sealTimings[ix].Unseal = unseal.Sub(verifySeal) } return nil }() if sealerr != nil { errs <- sealerr return } errs <- nil }(wid) } for i := 0; i < par.PreCommit1; i++ { err := <-errs if err != nil { return nil, nil, err } } return sealTimings, sealedSectors, nil } var proveCmd = &cli.Command{ Name: "prove", Usage: "Benchmark a proof computation", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "no-gpu", Usage: "disable gpu usage for the benchmark run", }, &cli.StringFlag{ Name: "miner-addr", Usage: "pass miner address (only necessary if using existing sectorbuilder)", Value: "t01000", }, }, Action: func(c *cli.Context) error { if c.Bool("no-gpu") { err := os.Setenv("BELLMAN_NO_GPU", "1") if err != nil { return xerrors.Errorf("setting no-gpu flag: %w", err) } } if !c.Args().Present() { return xerrors.Errorf("Usage: lotus-bench prove [input.json]") } inb, err := ioutil.ReadFile(c.Args().First()) if err != nil { return xerrors.Errorf("reading input file: %w", err) } var c2in Commit2In if err := json.Unmarshal(inb, &c2in); err != nil { return xerrors.Errorf("unmarshalling input file: %w", err) } if err := paramfetch.GetParams(lcli.ReqContext(c), build.ParametersJSON(), c2in.SectorSize); err != nil { return xerrors.Errorf("getting params: %w", err) } maddr, err := address.NewFromString(c.String("miner-addr")) if err != nil { return err } mid, err := address.IDFromAddress(maddr) if err != nil { return err } spt, err := ffiwrapper.SealProofTypeFromSectorSize(abi.SectorSize(c2in.SectorSize)) if err != nil { return err } cfg := &ffiwrapper.Config{ SealProofType: spt, } sb, err := ffiwrapper.New(nil, cfg) if err != nil { return err } start := time.Now() proof, err := sb.SealCommit2(context.TODO(), abi.SectorID{Miner: abi.ActorID(mid), Number: abi.SectorNumber(c2in.SectorNum)}, c2in.Phase1Out) if err != nil { return err } sealCommit2 := time.Now() fmt.Printf("proof: %x\n", proof) fmt.Printf("----\nresults (v27) (%d)\n", c2in.SectorSize) dur := sealCommit2.Sub(start) fmt.Printf("seal: commit phase 2: %s (%s)\n", dur, bps(abi.SectorSize(c2in.SectorSize), dur)) return nil }, } func bps(data abi.SectorSize, d time.Duration) string { bdata := new(big.Int).SetUint64(uint64(data)) bdata = bdata.Mul(bdata, big.NewInt(time.Second.Nanoseconds())) bps := bdata.Div(bdata, big.NewInt(d.Nanoseconds())) return types.SizeStr(types.BigInt{Int: bps}) + "/s" }