Repository navigation
test(umbp): gate the five client primitives on concurrency in CI - #634
Merged
Merged
Conversation
Adds umbp_prim_ci.py, which brings up standalone-process mode -- a umbp_standalone_server, and for the second arm a umbp_master in front of it -- runs umbp_prim_bench.py over 1/2/4/8 concurrent client processes and turns the rows into a verdict. Wired into the umbp-unit-test job. Two things are gated. Correctness, on every pass: put / get / exists / put_ranges / get_ranges must report no failed operations and no read-back mismatches at any rank count. Verification now compares every key over its whole object, through the op's own call, so get_ranges is checked with its real offset and size tables -- the only way a wrong range offset is catchable. The payload is a keyed pseudo-random stream, because a repeated tag verifies clean against a shifted read. And batch_exists throughput must still grow with rank count. A control plane that serialises reads looks healthy at one rank and collapses at eight, which no single-concurrency latency number can see, and comparing absolute numbers across runs on a shared machine is noise. That gate needs its own window. Measured through the correctness pass's geometry the timed window is under a millisecond, and three identical runs of one unmodified build scattered 1.59x / 1.88x / 3.49x, not even monotonic in rank count. Run as its own pass -- exists alone, 8192 keys/rank, 4 KiB objects, 100 passes -- the same build gives 3.50x to 5.39x over four runs with a one-rank baseline stable to 3%. The floor is therefore 2.0: ~43% under the worst healthy run observed, and 2.7x over the 0.65x that a known control-plane regression scores on the same node. exists is the gated primitive because it is the only one of the five that moves no bytes, so its scaling is a property of the control plane rather than of how much memory bandwidth the runner had left. The other four are measured and printed but not gated: both a good and a regressed build have been observed to anti-scale on the writes, so a floor there would be either vacuous or permanently red. The probe is also swept over the fraction of keys that were never written (0 / 0.5 / 1), since a resident key is resolved locally while an absent one is not. Only the 0 point is gated; the others have no measured baseline yet and report as info. Correctness IS checked on those passes, so a probe answering True for an absent key fails the build -- for a prefix-cache probe that is the more dangerous direction. The pool is charged in whole pages per key rather than in bytes. The DRAM medium is paged, so a 64 KiB object in a 2 MiB page occupies 2 MiB; sizing by bytes ran the pool 32x short, and because the just-seeded keys are the least recently used it surfaced as "seeded keys never became visible" rather than as anything about space. test_umbp_prim_gate.py replays both arms of that regression through the gate, so the floor cannot be retuned past what it exists to catch. It is pure arithmetic -- no mori, no server, no GPU -- so it runs in the ordinary pytest step. umbp_bench.py is removed. Its concurrent_perf subcommand is superseded by the rank sweep, and the compound flags the rest of it depended on were already deleted; what does not come across is listed in the PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TianDi101
merged commit Sep 3, 2026
0a46875
into
refactor/umbp-backend-agnostic
2 of 6 checks passed
TianDi101
added a commit
that referenced
this pull request
Sep 3, 2026
Adds umbp_prim_ci.py, which brings up standalone-process mode -- a umbp_standalone_server, and for the second arm a umbp_master in front of it -- runs umbp_prim_bench.py over 1/2/4/8 concurrent client processes and turns the rows into a verdict. Wired into the umbp-unit-test job. Two things are gated. Correctness, on every pass: put / get / exists / put_ranges / get_ranges must report no failed operations and no read-back mismatches at any rank count. Verification now compares every key over its whole object, through the op's own call, so get_ranges is checked with its real offset and size tables -- the only way a wrong range offset is catchable. The payload is a keyed pseudo-random stream, because a repeated tag verifies clean against a shifted read. And batch_exists throughput must still grow with rank count. A control plane that serialises reads looks healthy at one rank and collapses at eight, which no single-concurrency latency number can see, and comparing absolute numbers across runs on a shared machine is noise. That gate needs its own window. Measured through the correctness pass's geometry the timed window is under a millisecond, and three identical runs of one unmodified build scattered 1.59x / 1.88x / 3.49x, not even monotonic in rank count. Run as its own pass -- exists alone, 8192 keys/rank, 4 KiB objects, 100 passes -- the same build gives 3.50x to 5.39x over four runs with a one-rank baseline stable to 3%. The floor is therefore 2.0: ~43% under the worst healthy run observed, and 2.7x over the 0.65x that a known control-plane regression scores on the same node. exists is the gated primitive because it is the only one of the five that moves no bytes, so its scaling is a property of the control plane rather than of how much memory bandwidth the runner had left. The other four are measured and printed but not gated: both a good and a regressed build have been observed to anti-scale on the writes, so a floor there would be either vacuous or permanently red. The probe is also swept over the fraction of keys that were never written (0 / 0.5 / 1), since a resident key is resolved locally while an absent one is not. Only the 0 point is gated; the others have no measured baseline yet and report as info. Correctness IS checked on those passes, so a probe answering True for an absent key fails the build -- for a prefix-cache probe that is the more dangerous direction. The pool is charged in whole pages per key rather than in bytes. The DRAM medium is paged, so a 64 KiB object in a 2 MiB page occupies 2 MiB; sizing by bytes ran the pool 32x short, and because the just-seeded keys are the least recently used it surfaced as "seeded keys never became visible" rather than as anything about space. test_umbp_prim_gate.py replays both arms of that regression through the gate, so the floor cannot be retuned past what it exists to catch. It is pure arithmetic -- no mori, no server, no GPU -- so it runs in the ordinary pytest step. umbp_bench.py is removed. Its concurrent_perf subcommand is superseded by the rank sweep, and the compound flags the rest of it depended on were already deleted; what does not come across is listed in the PR. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the primitive benchmark into a CI gate, and removes
umbp_bench.py.umbp_prim_ci.pybrings up standalone-process mode — aumbp_standalone_server, and for the second arm aumbp_masterin front of it — runsumbp_prim_bench.pyover 1/2/4/8 concurrent client processes, and turns the rows into a verdict. It runs in the existingumbp-unit-testjob.What is gated
Correctness, on every pass. All five primitives must report no failed operations and no read-back mismatches at every rank count. Verification now compares every key over its whole object, through the op's own call — so
get_rangesis checked with its real offset and size tables, which is the only way a wrong range offset is catchable. The payload is a keyed pseudo-random stream; a repeated tag verifies clean against a shifted read.batch_existsthroughput must still grow with rank count. A control plane that serialises reads looks healthy at one rank and collapses at eight — no single-concurrency latency number can see that, and comparing absolute numbers across runs on a shared machine is noise. A ratio measured inside one run is not.Why the floor is 2.0, and why the measurement is a separate pass
Measured through the correctness pass's geometry the
existstimed window is under a millisecond, and three identical runs of one unmodified build scattered 1.59x / 1.88x / 3.49x, not even monotonic in rank count. A floor of 2.0 would have failed two of the three.Run as its own pass (
existsalone, 8192 keys/rank, 4 KiB objects, 100 passes) the same build gives 3.50x–5.39x over four runs, monotonic, one-rank baseline stable to 3%. So the floor sits ~43% under the worst healthy run seen and 2.7x over the regression it is aimed at. If this number ever reads noisy, raise--scaling-passes— a longer window — not the floor.Verified against both arms of the
09876ba7control-plane regression on one node, same geometry, same session:exists1→8a6970dc109876ba70.65x independently reproduces the 0.75x recorded for that regression on a different node and geometry. Both arms pass correctness — it is a slowdown, not a functional failure, and the gate catches it on the scaling axis alone.
existsis the gated primitive because it is the only one of the five that moves no bytes, so its scaling is a property of the control plane rather than of how much memory bandwidth the runner had left. The other four are measured and printed but not gated: both a good and a regressed build have been observed to anti-scale on the writes, so a floor there would be either vacuous or permanently red.Absent-key sweep
The probe is swept over the fraction of keys that were never written (0 / 0.5 / 1) — a resident key is resolved locally, an absent one is not. Only the 0 point is gated; the others have no measured baseline yet and report as
info. Correctness is checked on those passes, so a probe answeringTruefor an absent key fails the build — for a prefix-cache probe that is the more dangerous direction.Note for reviewers: on the nomaster arm absent keys are measurably faster (1.08M vs 0.95M keys/s), because with no master a local miss is conclusive. The "absent keys cost a master round trip" argument applies to the master arm only.
Pool sizing is charged in pages
The DRAM medium is paged and hands out whole pages, so a 64 KiB object in a 2 MiB page occupies 2 MiB. Sizing by
keys * object_bytesran the pool 32x short; because the just-seeded keys are the least recently used, that surfaced asseeded keys never became visiblerather than as anything about space — and it cost one false A/B result before it was found.pool_bytes()now chargesceil(object / page)pages per key and pins the page size on both arms.umbp_bench.pyremovedIts
concurrent_perfsubcommand is superseded by the rank sweep; the compound flags the rest depended on were deleted earlier. The same five APIs are exercised, but four measurements built on them do not come across:correctness: every key vs its full payloadexists_perf: sweeps batch[1, 32, 128, n]--batchper invocationbatch_perf:--dest-layout shuffleddefeats SGE coalescingranged_perf: ranged vs whole-object as a ratio per fetched fractionAlso gone: the DSv4-Pro pool presets, the
--rolewriter/reader split, and the Mooncake comparison path themooncake-vs-umbp-benchskill drove (that skill is now marked broken). The last committed version stays recoverable atgit show 09876ba7:tests/python/umbp/umbp_bench.py.Media selection — wired, but only
dramworks--media dram|hbm|ssd|dram:70,ssd:30is present and unit-tested, and generates a backend-policy file for mixtures (HBM is policy-only:Validate()demandshbm.capacity_bytes > 0and no env var sets it). Only--media dram, the CI default, is verified end to end.ssdfails every put with[SsdBackend] Resolve: staging arena busy; rolled back 0 reservations— rolled back zero, with 256 free pages against a 16-key batch, so it is not arena sizing;AcquireStagingSpanLockedreturns empty forpage_count == 0, which points inside the backend. Mixtures load the policy but the server then reportsmedium=[DRAM ...]with no SSD backend. Both are recorded in the module header. Happy to split this out of the PR if you would rather it landed only once it works.Tests
test_umbp_prim_gate.pyreplays both arms of the regression through the gate, so the floor cannot be retuned past what it exists to catch, and pins the page-charged pool arithmetic.test_umbp_prim_bench.pycovers the key spaces, payload and absent-key mixing. Both are pure arithmetic — no mori, no server, no GPU — so they run in the ordinary pytest step. 44 new tests.Cost
The job's
timeout-minutesgoes 30 → 60 and the run asks the server for a ~35 GiB pool at CI defaults. Worth a look if the runner is tighter than I assume.🤖 Generated with Claude Code