Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

immutable validator database factoring #2297

Merged
merged 38 commits into from
Mar 15, 2021
Merged

immutable validator database factoring #2297

merged 38 commits into from
Mar 15, 2021

Conversation

tersec
Copy link
Contributor

@tersec tersec commented Feb 6, 2021

Shows the approximate gains one can get though, and the general approach.

This is part of state diffs as a whole, but a prerequisite and is separately useful.

Overall ncli_db bench --slots=150000 times with mainnet blocks, v1.0.7 (first row) vs this PR (second row):

8	1111	1144.7	1185.16
12	868.3	892.1	916.8

where columns are (n, 25th percentile, mean, 75th percentile) in seconds. It's about 20% faster overall.

For "Database block store" specifically, which here includes state storage in milliseconds, it's about 60% faster:

8	3.31	3.33	3.34
12	1.37	1.40	1.41

Typical ncli_db summaries look like, for v1.0.7:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    7474.769,        0.000,     7474.769,     7474.769,            1, Initialize DB
       0.459,        1.466,        0.058,      474.874,       147164, Load block from database
     512.211,        0.000,      512.211,      512.211,            1, Load state from database
       0.118,        0.224,        0.011,       75.854,       145313, Advance slot, non-epoch
      15.556,        4.353,        2.299,       47.374,         4687, Advance slot, epoch
       1.873,        7.082,        0.016,      424.647,       147164, Apply block, no slot processing
       3.334,       20.061,        0.001,     1404.539,       147164, Database block store

and for this branch:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    6318.330,        0.000,     6318.330,     6318.330,            1, Initialize DB
       0.526,        1.967,        0.063,      679.893,       147164, Load block from database
     549.847,        0.000,      549.847,      549.847,            1, Load state from database
       0.119,        0.246,        0.011,       87.330,       145313, Advance slot, non-epoch
      15.897,        4.659,        2.850,       51.757,         4687, Advance slot, epoch
       1.875,        7.155,        0.014,      306.680,       147164, Apply block, no slot processing
       1.354,        8.387,        0.001,     1000.602,       147164, Database block store

For database size, v1.0.7 (with noted ncli_db patch) creates a 17GiB benchmark/nbc.sqlite3 file snapshotting every epoch, while this branch creates a 3.9GiB benchmark/nbc.sqlite3 file under the same conditions, for a 75% reduction in size, and, presumably, disk I/O. The gap increases with slot number.

For v1.0.7 benchmarking, I first applied:

diff --git a/ncli/ncli_db.nim b/ncli/ncli_db.nim
index 04b9d58d..3aa77a41 100644
--- a/ncli/ncli_db.nim
+++ b/ncli/ncli_db.nim
@@ -93,7 +93,9 @@ proc cmdBench(conf: DbConf, runtimePreset: RuntimePreset) =
   let
     db = BeaconChainDB.init(runtimePreset, conf.databaseDir.string)
     dbBenchmark = BeaconChainDB.init(runtimePreset, "benchmark")
-  defer: db.close()
+  defer:
+    db.close()
+    dbBenchmark.close()
 
   if not ChainDAGRef.isInitialized(db):
     echo "Database not initialized"
@@ -151,6 +153,10 @@ proc cmdBench(conf: DbConf, runtimePreset: RuntimePreset) =
     if conf.storeBlocks:
       withTimer(timers[tDbStore]):
         dbBenchmark.putBlock(b)
+    withTimer(timers[tDbStore]):
+      if state[].data.slot mod SLOTS_PER_EPOCH == 0:
+        dbBenchmark.putState(state[].root, state[].data)
+      dbBenchmark.checkpoint()
 
   printTimers(false, timers)

To simulate the basic operation of state storage by chain_dag. It doesn't account for some of the finalization pruning, but that only shifts the total amounts, not the ratios/proportions here. Similarly, without checkpointing, it just builds up the WAL file, so it checkpoints exactly as often as nbc.

doAssert immutableValidators.len >= intermediateOutput[].validators.len

# TODO factor out, maybe
for i in 0 ..< intermediateOutput[].validators.len:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the output has validators, we can skip "updating" them here.

also, this code should use a pointer trick (unfortunately) to assign the fields, ie

   let validator = addr validators[i]
   assign(validator[], immutableValidators[i].withdrawal_credentials)
   ...

or it will keep invalidating the cache for the same entry over and over which is quite inefficient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a neater way to solve it is actually to add func assign(tgt: var Validator, src: immutableValidator)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this version's faster, but I reimplemented this portion because it seemed to be a reasonable way to add an item to the end of the List while simultaneously constructing the object. If it's still better to add a default object, then fill it in with assign, this can be revisited.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, really the thing that makes the most sense is to update output.validators rather than overwriting it then readding the entries one by one - this will avoid 99% of the copying every time because the immutable validator set is.. well.. immutable..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would essentially mean turning getBeaconStateNoImmutableValidators into an updateBeaconStateNoImmutableValidators

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would play well with how getstate is typically use: we pass it some random but valid state that usually is "close" to what we're looking for, ie different fork, a few slots back etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tersec tersec force-pushed the lpt branch 2 times, most recently from 615bb9a to cf73c6c Compare March 4, 2021 19:00
@tersec tersec changed the title [WIP] initial immutable validator database factoring initial immutable validator database factoring Mar 4, 2021
@tersec
Copy link
Contributor Author

tersec commented Mar 5, 2021

Updated unstable (as of this comment's writing) vs this branch benchmarks:

Total ncli_db runtime, with first row being unstable and second row being this branch, unit seconds, columns are sample size, 25th percentile, mean, and 75th percentile:

4	1113.4075	1136.3425	1160.83
5	889.9	861.722	921.34

So this branch applies blocks from the first 150k slots using ncli_db bench --slots=150000 --storeStates=true about than 25% faster, overall, than unstable.

A typical unstable ncli_db summary:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    6506.706,        0.000,     6506.706,     6506.706,            1, Initialize DB
       0.332,        0.622,        0.043,      166.005,       147164, Load block from database
     372.870,        0.000,      372.870,      372.870,            1, Load state from database
       0.115,        0.222,        0.011,       81.439,       145313, Advance slot, non-epoch
      15.489,        3.986,        2.138,       33.143,         4687, Advance slot, epoch
       1.929,        7.295,        0.014,      117.954,       147164, Apply block, no slot processing
       2.931,       17.399,        0.001,      374.989,       147164, Database store

for unstable and

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    6960.661,        0.000,     6960.661,     6960.661,            1, Initialize DB
       0.534,        1.133,        0.064,      300.120,       147164, Load block from database
     451.754,        0.000,      451.754,      451.754,            1, Load state from database
       0.116,        0.227,        0.011,       83.366,       145313, Advance slot, non-epoch
      15.415,        4.034,        1.990,       34.738,         4687, Advance slot, epoch
       1.846,        7.070,        0.010,      115.891,       147164, Apply block, no slot processing
       1.177,        7.002,        0.001,      450.747,       147164, Database store

for this branch.

While other rows are noisy, but not consistently different between the two branches, the database store is about 60% faster in this branch on average than unstable, while still creating the same 17GB/3.9GB file size split. I suspect the maximum database store time being higher than in this branch than unstable is for overhead + some of the initial states where there are lots of genesis validators to bring in, so less gain from the immutable validator setup.

@arnetheduck
Copy link
Member

one thing the current bench suite doesn't do is reload states - might be a worthy addition to have

@tersec
Copy link
Contributor Author

tersec commented Mar 8, 2021

one thing the current bench suite doesn't do is reload states - might be a worthy addition to have

436ab76

It's slow, so it's only doing it every 32 states stored. Maybe that ratio should be even higher.

This branch, 100k validators, running build/ncli_db bench --slots=100000 --storeStates=true:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    4120.077,        0.000,     4120.077,     4120.077,            1, Initialize DB
       0.141,        0.359,        0.031,       73.134,        97840, Load block from database
      30.612,        0.000,       30.612,       30.612,            1, Load state from database
       0.167,        0.442,        0.026,      135.783,        96875, Advance slot, non-epoch
      12.006,        2.268,        1.580,       19.833,         3125, Advance slot, epoch
       1.303,        4.783,        0.014,       66.551,        97840, Apply block, no slot processing
    1275.156,      403.390,      536.833,     1941.837,           90, Database load
      19.673,        4.158,       11.256,       52.660,         2862, Database store

unstable:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    4184.500,        0.000,     4184.500,     4184.500,            1, Initialize DB
       0.162,        0.333,        0.033,       79.343,        97840, Load block from database
      39.811,        0.000,       39.811,       39.811,            1, Load state from database
       0.163,        0.365,        0.026,      111.225,        96875, Advance slot, non-epoch
      11.259,        2.266,        1.434,       26.097,         3125, Advance slot, epoch
       1.307,        4.862,        0.016,       74.821,        97840, Apply block, no slot processing
      30.193,       11.615,       10.536,       60.297,           90, Database load
      65.067,       12.983,       24.297,      146.907,         2862, Database store

These results aren't comparable to the others here, because they're on a different system (NVMe SSD, but probably slower CPU).

They do point to a weak point of this PR that needs improving before merging, though. It's good that stores remain several times faster, and the space savings are nice, but pessimizing load this much isn't reasonable.

@tersec
Copy link
Contributor Author

tersec commented Mar 8, 2021

After 8fe0f1d

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    4932.986,        0.000,     4932.986,     4932.986,            1, Initialize DB
       0.122,        0.285,        0.032,       70.069,        97840, Load block from database
      37.781,        0.000,       37.781,       37.781,            1, Load state from database
       0.163,        0.395,        0.026,      121.307,        96875, Advance slot, non-epoch
      11.751,        2.231,        1.434,       16.670,         3125, Advance slot, epoch
       1.243,        4.472,        0.013,       50.591,        97840, Apply block, no slot processing
      42.847,        7.225,       30.379,       79.228,         2862, Database load
      18.231,        7.148,        7.753,      344.007,         2862, Database store

@arnetheduck
Copy link
Member

uh, yeah, that load looks completely weird - just like less data is being stored, loading less data should be faster..

@tersec
Copy link
Contributor Author

tersec commented Mar 8, 2021

It's loading in an entire BeaconStateNoImmutableValidators, then copying it over to a BeaconState. There's less disk I/O, but it's still doing a lot of additional work.

There's also a reason I commented in the code since the beginning that the correct way of doing that part isn't this copying approach, but reading the SSZ directly into a BeaconState. It'd just not yet been the main remaining bottleneck.

It also allocates a wholly new BeaconState each time, which is easy enough to remove, and, as you point out elsewhere in this PR, mostly pointlessly copies the immutable validator data.

@tersec tersec changed the title initial immutable validator database factoring immutable validator database factoring Mar 9, 2021
@tersec
Copy link
Contributor Author

tersec commented Mar 9, 2021

After f0a5f65

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    3869.052,        0.000,     3869.052,     3869.052,            1, Initialize DB
       0.077,        0.225,        0.026,       57.733,        97840, Load block from database
      27.120,        0.000,       27.120,       27.120,            1, Load state from database
       0.139,        0.357,        0.025,      109.845,        96875, Advance slot, non-epoch
       9.587,        1.884,        1.310,       15.030,         3125, Advance slot, epoch
       1.013,        3.644,        0.014,       46.519,        97840, Apply block, no slot processing
       9.843,        5.015,        3.547,       36.852,         2862, Database load
      17.638,        6.347,        9.191,      304.218,         2862, Database store

Evidently, 3/4 of the database load time had been related to that BeaconState allocation.

@tersec
Copy link
Contributor Author

tersec commented Mar 9, 2021

this looks fishy - it'll clear the top-level cache but not the tree under it - why does it work?

It doesn't, when it actually tries to reuse the BeaconState. That's not one of the unit tests, and should be. The unit tests always use a new BeaconState. I caught it reproducibly with ncli_db and added a test case for it.

4b93c6d has some brute-force fixes for this, which still seem to perform adequately:

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    3552.157,        0.000,     3552.157,     3552.157,            1, Initialize DB
       0.075,        0.221,        0.026,       56.410,        97840, Load block from database
      27.065,        0.000,       27.065,       27.065,            1, Load state from database
       0.142,        0.321,        0.025,       98.401,        96875, Advance slot, non-epoch
       9.672,        2.026,        1.270,       15.922,         3125, Advance slot, epoch
       0.973,        3.529,        0.012,       44.786,        97840, Apply block, no slot processing
       9.870,        3.873,        3.766,       39.895,         2862, Database load
      15.654,        6.192,        7.277,      271.947,         2862, Database store

@tersec
Copy link
Contributor Author

tersec commented Mar 9, 2021

x: var HashList[...]

in other places (like assign2) the params are called tgt and src

4b93c6d

@tersec
Copy link
Contributor Author

tersec commented Mar 9, 2021

After d41c429

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    3961.175,        0.000,     3961.175,     3961.175,            1, Initialize DB
       0.083,        0.242,        0.026,       62.136,        97840, Load block from database
      29.990,        0.000,       29.990,       29.990,            1, Load state from database
       0.147,        0.351,        0.025,      107.537,        96875, Advance slot, non-epoch
      11.037,        2.275,        1.258,       17.422,         3125, Advance slot, epoch
       1.126,        4.083,        0.015,       47.870,        97840, Apply block, no slot processing
      10.532,        4.416,        3.802,       35.342,         2862, Database load
      17.505,        6.478,        7.847,      295.336,         2862, Database store

It seems to be stabilizing around there. The BeaconState memory allocation was the big one.

It does suggest that the BeaconStateNoImmutableValidators probably is a problem too.

…ffers; add ncli_db and a unit test to verify this
@@ -582,6 +582,11 @@ type
ValidatorStatus* = object
# This is a validator without the expensive, immutable, append-only parts

pubkey* {.dontserialize.}: ValidatorPubKey
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do these need to be here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They enable

proc putState*(db: BeaconChainDB, key: Eth2Digest, value: var BeaconState) =
# TODO prune old states - this is less easy than it seems as we never know
# when or if a particular state will become finalized.
updateImmutableValidators(db, db.immutableValidatorsMem, value.validators)
db.put(
subkey(BeaconStateNoImmutableValidators, key),
cast[ref BeaconStateNoImmutableValidators](addr value)[])

and
case db.get(
subkey(
BeaconStateNoImmutableValidators, key),
cast[ref BeaconStateNoImmutableValidators](addr output)[])

which is clarified slightly by

static:
doAssert sizeof(Validator) == sizeof(ValidatorStatus)
doAssert sizeof(BeaconState) == sizeof(BeaconStateNoImmutableValidators)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither ValidatorStatus nor BeaconStateNoImmutableValidators is an object type that's ever exactly instantiated in this version; they're most usefully considered as instructions to the SSZ (de)serializer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, let's document that in the type? it's.. a bit weird - ie the intent is to match the ABI basically, so it's good to write that down

Copy link
Contributor Author

@tersec tersec Mar 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c4cf2a8

From #2297 (comment) and #2297 (comment) one can see the improvement from this approach, from

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    1340.984,        0.000,     1340.984,     1340.984,            1, Initialize DB
       0.327,        0.987,        0.079,      255.120,        91447, Load block from database
      61.806,        0.000,       61.806,       61.806,            1, Load state from database
       0.627,        1.463,        0.025,      446.327,        96875, Advance slot, non-epoch
      41.311,        3.812,       10.844,       72.374,         3125, Advance slot, epoch
       3.447,        7.850,        0.057,      252.413,        91447, Apply block, no slot processing
      24.812,        6.950,       17.393,       84.707,         2348, Database load
      37.135,        4.519,       24.421,       73.783,         2348, Database store

real	13m10.429s
user	12m2.704s
sys	0m30.644s

to

All time are ms
     Average,       StdDev,          Min,          Max,      Samples,         Test
Validation is turned off meaning that no BLS operations are performed
    1234.699,        0.000,     1234.699,     1234.699,            1, Initialize DB
       0.377,        0.694,        0.071,      133.208,        91447, Load block from database
      49.274,        0.000,       49.274,       49.274,            1, Load state from database
       0.566,        1.264,        0.025,      385.073,        96875, Advance slot, non-epoch
      39.309,        2.966,        9.613,       49.510,         3125, Advance slot, epoch
       3.163,        7.633,        0.042,      233.757,        91447, Apply block, no slot processing
      18.837,        5.613,        9.518,       53.715,         2348, Database load
      32.238,       29.068,       17.496,     1401.262,         2348, Database store

real	12m4.686s
user	10m54.210s
sys	0m30.473s

Average database load times decreased by 25%, database store times by more than 10%, and overall ncli_db runtime by almost 10% on the first 100k Pyrmont slots.

I also ran ncli_db pruneDatabase on my Pyrmont node's 19GB database, which it compressed to about 11GB, for a 40% decrease in overall database size. This is reduced from the 85% here due to all the blocks, which remain unchanged, but it's still a useful improvement. It won't be representative of nbc until the full-state-writing can be dropped, though.

doAssert s.isOk # TODO(zah) Handle this in a better way
var sqliteStore =
if inMemory:
SqStoreRef.init("", "test", inMemory = true).expect("working database (disk broken/full?)")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, the joys of copy-paste :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tersec tersec merged commit 8def248 into unstable Mar 15, 2021
@tersec tersec deleted the lpt branch March 15, 2021 14:11
This was referenced Mar 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants