misc: update doc - #715
Merged
Merged
misc: update doc#715
Conversation
Ying1123
approved these changes
Jul 24, 2024
timethink
pushed a commit
to timethink/sglang
that referenced
this pull request
Mar 9, 2025
cen121212
pushed a commit
to cen121212/sglang
that referenced
this pull request
Nov 10, 2025
<!-- Thank you for your contribution! Please follow these guidelines to enhance your pull request. If anything is unclear, submit your PR and reach out to maintainers for assistance. Join our Slack community at https://slack.sglang.ai to discuss further. --> ## Motivation <!-- Describe the purpose and goals of this pull request. --> ## Modifications <!-- Detail the changes made in this pull request. --> ## Accuracy Tests <!-- If this pull request affects model outputs (e.g., changes to the kernel or model forward code), provide accuracy test results. --> ## Benchmarking and Profiling <!-- If this pull request impacts inference speed, provide benchmarking and profiling results. --> ## Checklist - [x] Format your code according to the [Format code with pre-commit](https://docs.sglang.ai/developer_guide/contribution_guide.html#format-code-with-pre-commit). - [x] Add unit tests according to the [Run and add unit tests](https://docs.sglang.ai/developer_guide/contribution_guide.html#run-and-add-unit-tests). - [x] Update documentation according to [Write documentations](https://docs.sglang.ai/developer_guide/contribution_guide.html#write-documentations). - [x] Provide accuracy and speed benchmark results according to [Test the accuracy](https://docs.sglang.ai/developer_guide/contribution_guide.html#test-the-accuracy) and [Benchmark the speed](https://docs.sglang.ai/developer_guide/contribution_guide.html#benchmark-the-speed).
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
… explain Measured 2026-08-17 02:18:09, all three ranks, inside the OOM this reporter (sgl-project#695) was written to diagnose: RADIX SHAPE: walk failed after 3 nodes (RuntimeError('Boolean value of Tensor with more than one value is ambiguous')). Partial: tokens=1, locked_nodes=1. Cause, and it is mine: `len(getattr(node, "value", ()) or ())`. node.value is a TENSOR, so `tensor or ()` evaluates bool(tensor), which raises for any tensor with more than one element. The walk therefore survived only empty or single-element nodes -- an empty tree -- and fell over on the first real one. It has presumably never worked on a populated tree since it was written. A diagnostic that only works when there is nothing to diagnose is not a diagnostic, and this one failed at precisely the moment its output was the evidence needed: the tree shape at the crash would have shown whether the 147,456 tokens the counter certified were reachable from the unlocked leaf frontier the actuator walks. That question is still open BECAUSE of this bug. Length is now asked for directly and never via truthiness. Tests, hermetic (CUDA_VISIBLE_DEVICES=""): test_multi_element_tensor_value_does_not_break_the_walk .. the specimen shape test_counts_tokens_across_a_deeper_tree ................. 100+30+7 counted test_none_and_empty_values_still_count_zero ............. CAN-FAIL boundary: the fix must not crash on a missing value nor count None as a token test_a_locked_multi_element_node_is_still_counted_as_locked -> 4 passed + 2 subtests; 16 passed across the 715 + 695 + 694 mem_cache suites. ruff clean. CAN-FAIL PROVEN BY MUTATION: restoring the original expression fails 5 of 6; reverted and re-verified 4 passed. The suite would have caught the bug on the day it was written.
efschu
added a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…nothing closed it Desk analysis of the 3x FLIP ABANDONED on 0b61699, plus the guard that makes the real state legible. 1. THE 1748-vs-1693 ARITHMETIC, reconstructed. phase_flip_runtime.py:4993-5019: usable = from_driver = max(0, driver_free - reserve) spendable = usable # allocator cache is NOT counted So "spendable" is driver-free minus the kept-free reserve, measured AFTER _reclaim_cached_blocks() has returned what it can; whatever the caching allocator still holds is deliberately excluded. The 1748 requirement comes from _staging_bytes, which is incoming + max(outgoing, local) over the WIDEST WAVE, using the same row_nbytes the move itself uses -- derived from the plan, not estimated, and waved so it scales with pool geometry rather than prompt length. I find no overcount on that side. 2. THE RUNG LINE IS ARITHMETICALLY IMPOSSIBLE, and that is the root. _floor_rows is max_live + 1 + margin_rows + admission_reserve_rows. margin_rows DEFAULTS TO 0 and is never passed at the construction site (kv_backing_relief.py:2158-2167), and admission_reserve_rows is chunked_prefill_size = 512. So floor = max_live + 513, and floor=398471 means max_live = 397,958 -- against a current cap of 137,216. A live row id 2.9x ABOVE the cap. Compare the healthy shape this module documents itself (line 875): "max_live=644 + admission reserve 512, slack=405894". There the high-water id is tiny. Here it outlived the pool it was measured in: ids from a larger id space surviving a reshard/shrink. Because slack is max(0, current - floor_rows), it pins to 0 for as long as that holds, so the rung can NEVER propose a shrink. The evict-rung funding path (sgl-project#688) is therefore permanently unavailable at this operating point, and every flip falls back on the raw seam fund alone. That is why the instance abandoned three times over 55 MiB instead of funding it from KV once -- the backstop was gone, not merely small. So: the floor FORMULA is right and its INPUT is impossible. The defect is upstream of this file, in whatever leaves live ids above the cap. 3. WHAT LANDS HERE. Only the guard: a floor above the cap now says FLOOR UNREACHABLE, names the gap in rows, and states the implication (this rung can never fund; max_live is above the cap). "slack=0" alone is indistinguishable from a rung that merely had no room this round, which is exactly the confusion this ticket started in. The healthy path is untouched and a floor exactly AT the cap is reported as tight, not impossible. I did NOT change the floor, the eviction handoff, or the live-set derivation. Re-basing live ids after a shrink/reshard is the actual fix and it sits in the area F4-r4 holds for sgl-project#715, so per the coordination rule it goes back through the operator rather than being edited here. Tests: 5, red first -- two new assertions failed, three controls (healthy shape, boundary at floor == cap, never-ran) passed before the change and still do. managers + mem_cache: 2892 passed, 0 failed. ruff clean.
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…ts is unreachable Pre-approved review amendment: _post_evict_rows() was the one undefended term inside the diagnostic's logger call, and a raise there would kill the scheduler round the line exists to observe -- the sgl-project#715 failure mode exactly. It now uses the same try/except-RAISED pattern as the other two probes. BUT THE MUTATION SAYS THE CONCERN IS UNREACHABLE, AND I WILL NOT DRESS THAT UP. My first test for it broke the accessors underneath _post_evict_rows and passed 10/10 against a deliberately UNDEFENDED call site -- a vacuous test, caught only because I mutation-check every can-fail. The reason: _post_evict_rows swallows its own accessor exceptions internally, so it cannot raise. Forcing a raise means patching the bound method, which then explodes inside _layout_admits (also a bare call) before the diagnostic is ever reached -- testing neither. So: the hardening stays, as cheap insurance against a future edit that makes _post_evict_rows raise, and because a uniform pattern is worth more than a justified exception. It is NOT claimed to fix a reachable defect today, the unreachable arm is removed rather than left as a passing test that proves nothing, and the limit is written into the test class docstring so the next reader does not re-derive it. The mamba probe IS reachable and remains tested: a raising accessor must be NAMED as "RAISED <type>", never silently read as 0, because zero-by-exception and zero-by-measurement are different states and only one is a defect. Tests, hermetic: 10 passed + 1 subtest; and the 677/689/708 adjacency suites pass alongside. ruff clean.
efschu
added a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…d their honest cost
Slice 1 verified a checkpoint's references and refused to branch when any had
been evicted. Correct failure, still a failure: the store is an LRU cache and a
conversation checkpoint is not cache-shaped -- its whole value is staying
restorable after its pages stop being hot. So a checkpoint now PINS what it
references, and the cost of that promise is accounted rather than hidden.
WHAT THE PIN DOES
* Eviction SKIPS pinned entries, using the same skip-and-repin step in-flight
writes already get. Deliberately the same: it reuses the bounded skip budget
in _evict_while, so a store whose entries are ALL pinned exhausts its
attempts and the caller learns the space is not there, instead of spinning.
* The TTL sweeper skips a pinned page's .part706 as well, because a checkpoint
may reference a page another stage is still assembling and age alone cannot
tell that from abandonment -- a pin can.
* Pins are REF-COUNTED by checkpoint. Branching exists to SHARE a prefix, so
unpinning one holder must not strip its sibling, and a branch that shares its
parent's whole prefix costs ZERO new pinned bytes (tested).
* Pins are DURABLE ({store}/pins706/{checkpoint}.pin.json, atomic replace). A
checkpoint outlives the process; a memory-only pin would silently stop
protecting anything at the next restart.
BUDGET HONESTY (sgl-project#715: never count as deliverable what the actuator cannot
deliver). capacity_stats now reports pinned_entries, pinned_bytes and
reclaimable_bytes = used - pinned, so a capacity decision reads the number
eviction can actually free. Creating a checkpoint whose pins would cross
SGLANG_HICACHE_PIN_BUDGET_BYTES is REFUSED with all four numbers (want, held,
budget, overshoot) and changes nothing -- a cache quietly degraded into a pin
museum is the outcome worth refusing.
UNIT BUG FOUND BY THE TESTS, and it was the sgl-project#715 sin in miniature: the ledger
charged APPARENT size (st_size) while the evictor accounts ALLOCATED size
(st_blocks; the incident filesystem charged 8704 bytes for a 512-byte page,
17x over 5.8M files). reclaimable = used - pinned was therefore subtracting
centimetres from inches, and could go NEGATIVE. The ledger now charges through
LRUFileEvictor._allocated_size -- one unit, one authority.
ORPHANS: a pin whose manifest is gone protects nothing and blocks eviction
forever -- the .part706 leak shape one layer up, reaped the same way. Age-gated
against the authority that knows whether the checkpoint exists, because a
checkpoint being written has pins BEFORE it has a manifest and reaping those
would delete the protection at the exact moment it is needed.
Tests (hermetic, CUDA_VISIBLE_DEVICES=""), 13 new, 181 in the family. The three
named red-first cases, all against the REAL evictor under REAL pressure:
* test_the_pinned_sibling_survives_eviction_pressure -- two identical pages,
one pinned, cap exceeded: the unpinned one dies, the pinned one is there.
* test_pinned_bytes_are_not_reported_as_reclaimable -- the arithmetic.
* test_an_orphan_pin_is_reaped (+ test_a_young_orphan_is_left_alone).
Plus ref-counting, restart durability, the budget refusal with its numbers,
zero-charge re-pinning, and the sweeper honouring pins.
Two test-construction bugs of my own, both found by running rather than
reading, both recorded because they are the same class: the eviction cap was
computed from tensor length while the evictor charges allocated blocks, so the
page was evicted before it could be pinned (the pin then correctly charged
nothing, and the test failed for a reason unrelated to pinning). The cap is now
MEASURED from a probe store -- which itself needs a cap, since byte accounting
only runs when eviction is configured.
Regression, same env, base c3e9487 vs this commit:
unit/mem_cache 940 failed / 779 passed -> 940 failed / 938 passed (+159)
unit/managers 4 failed / 2145 passed -> 4 failed / 2145 passed
Still slice 3, named not smuggled: wiring pin/unpin to the checkpoint create
and delete endpoints, which needs the live session layer.
CAN-FAIL PROOF (mutation applied, suite re-run, reverted):
T1 eviction ignores pins
-> test_the_pinned_sibling_survives_eviction_pressure,
test_a_store_of_only_pins_cannot_spin
T2 reclaimable reported as used (the sgl-project#715 sin, planted)
-> test_pinned_bytes_are_not_reported_as_reclaimable, +1
T3 unpin drops shared pages
-> test_pins_are_ref_counted_across_checkpoints, and ONLY that one
T4 the budget never refuses
-> test_the_budget_refuses_with_the_numbers, and ONLY that one
T5 orphan reaping ignores age
-> test_a_young_orphan_is_left_alone, and ONLY that one
T6 the sweeper ignores pins
-> test_a_pinned_pages_partial_is_not_reaped, and ONLY that one
T7 pins not persisted
-> test_pins_survive_a_restart, and ONLY that one
Restored tree re-verified green after every mutation (13 passed).
efschu
added a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…e they raise ROOT, and it is not a new one. free_group_begin is called from the event loop (batch_result_processor.py:92 and :741). While that window is open, PagedTokenToKVPoolAllocator.free appends to free_group instead of extending free_pages (allocator/paged.py:293-308), so the pages sit in neither free_pages nor release_pages and available_size cannot see them -- while the tree has already counted them as evicted. That is sgl-project#681's third root, and flush_free_group (allocator/base.py:208) is its remedy. WHY IT CRASHED AGAIN ANYWAY. The remedy was wired into alloc_token_slots only. The paged twins reached their raise without ever asking whether the pages they needed were already freed and merely staged. The relief NET was carried across to the extend path under "sgl-project#681 RULE 3: every alloc path reachable from prefill admission gets the same net"; the third root was not carried with it, and the decode path had no net of any kind. So: one root, wired on one of the three paths that need it. That is the 02:18 crash -- 512 tokens refused with 147,456 counted evictable. The receipt-checking added in sgl-project#681 cannot catch it, because the eviction's receipt is HONEST: the tokens really were freed. THE LABELLED CANDIDATE IS REFUTED. The proposal was that _evict_leaf_node's allocator.free(x.value) might route rows to one sub-pool of a HybridLinearKVPool while available_size/alloc read another. It cannot produce this divergence: the accounting is entirely allocator-side over index bookkeeping (free_pages / release_pages / free_group), and available_size is computed from those two lists alone (base.py:187-188), so a free and the available_size after it read the same structure however the pool splits its tensors underneath. Pinned in TestAccountingLivesInTheAllocator, including a run with kvcache=None throughout. This says only that the hybrid pool cannot cause THIS divergence, not that it is defect-free. FIX: flush staged frees and retry, on both paged paths, before the relief ladder -- same ordering as alloc_token_slots and for the same reason, that it is not relief. It gives up nothing: it applies frees already performed and already counted. Cold path only, reached after an allocation has already failed. The raises are unchanged, so fail-loud keeps the last word. Tests (hermetic, CUDA_VISIBLE_DEVICES="", no GPU, no serving contact): test/registered/unit/mem_cache/test_paged_staged_frees_715.py 9 passed can-fail proof: the two fix-pins fail without the fix, with the exact production messages ("Prefill out of memory" / "Decode out of memory"); the other 7 hold either way test/registered/unit/mem_cache/ 797 passed, 1651 skipped, 124 subtests test/registered/unit/managers/ 2119 passed, 18 skipped, 130 subtests ruff clean on both changed files The pins drive the REAL PagedTokenToKVPoolAllocator on CPU, inheriting the whole group protocol unmodified; only alloc_extend/alloc_decode are overridden, because the production ones dispatch to CUDA kernels (sgl-project#624: the stub stays off the load-bearing path).
efschu
added a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…omits TICKET STATE FIRST, because it changes what the work is: the corruption sgl-project#568 describes is ALREADY FIXED. Commit 4222976 ("ledger: park/restore replaced every non-persistent buffer with garbage") carries the park-side copy and the restore-side re-registration, it is present in the deploy tree (4 references to _cpu_nonpersistent) and absent from main, and three tests already cover it (test_a_non_persistent_buffer_comes_back_bit_identical and siblings). Verified load-bearing rather than assumed: reverting the park loop fails exactly those three tests and nothing else. So this commit is the REST of that omission -- the same "state_dict() is not the module's state" mistake, one layer up, in two places the original fix did not reach. 1. THE LEDGER DID NOT COUNT THEM. `tensors()` enumerated `state_dict()` only, so `size_bytes()` under-reported while `park()` went on to free the non-persistent buffers as well. Measured on a 64x64 parameter plus a 512-element inv_freq: registered 16,384 bytes, freed 18,432. The register prices victim choices on the registered number, and the residency report showed a module freeing more than it was ever registered as holding. FAILURE DIRECTION, stated: under-counting is the SAFE direction -- the actuator delivers more than promised, the mirror image of the sgl-project#715 sin rather than the sin itself -- and it costs accuracy, since the ladder can pass over a victim that would have sufficed. This cannot over-count: every tensor now listed is one the park really frees. 2. THE RESTORE GUARD DID NOT LOOK FOR THEM, and this one is sharp. A module whose ENTIRE state is non-persistent buffers -- a rotary cache is exactly that shape -- parks with an empty `_cpu_state` and a full `_cpu_nonpersistent`. The guard tested only the first and declared the module "parked but holds no host copy ... unrecoverable in place -- reload it from the checkpoint", while its bytes sat on the host one attribute away. A park that succeeded followed by a restore that refused. Before fix (1) it was worse still: such a module's `tensors()` was empty, so park took the "nothing to do" early exit and marked it parked having saved nothing. Both fixed at their single sources. The guard stays strict where it should: with BOTH stores empty the module really is unrecoverable, and a test pins that it still refuses. SIBLING SWEEP. The rule is module-GENERIC and always was: the ledger registers arbitrary modules by dotted path (inprocess_tts.py:274), so every asset class it will ever be handed goes through this path -- drafters, vocoder, speaker encoder alike. Nor is the affected state only `inv_freq`: the rotary variants in this tree also carry `short_cos_sin_cache`, `long_cos_sin_cache`, `cos_sin_q*_cache` and `axis_map` as non-persistent buffers. So the general rule is pinned rather than the single instance, over three module shapes including one made of nothing but such buffers. THE 19 COUNT IS NOT REPRODUCIBLE HERMETICALLY, and I will not pretend otherwise: it is an instance count from a loaded checkpoint, while the source carries 13 `register_buffer("inv_freq")` DEFINITION sites -- how many submodules a checkpoint instantiates is a property of the model. What the next translator test needs is therefore an instrument, and park now logs one: the persistent and non-persistent tensor counts per parked asset. Confirming "19" on metal is then reading a log line rather than guessing which submodules loaded. Tests: 4 new (26 in the file, 640 in the translator suite). The accounting rule is asserted over three module shapes; the only-buffers module survives the round trip byte-identically; it is no longer treated as empty; and a genuinely interrupted park is still refused. Also recorded: `git stash` is BANNED by a repo hook (shared stack across worktrees) -- it refused an attempt to A/B this fix, correctly, and the run that looked like a "before" was actually the fixed code. Use a patch file.
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
Step 2 of the reconciliation, and a correctness fix in its own right. A filesystem charges the blocks it allocated, not the length the file reports. On this rig's ZFS a 64-byte page occupies 512 bytes -- 8x -- and the incident that produced _allocated_size measured 512-byte pages occupying 8704 bytes each across 5.8M files. A store that believes it is using a fraction of its real disk does not evict when it must. It also unblocks sgl-project#410's pin ledger, which charges allocated bytes. While the evictor charged apparent ones, reclaimable = used - pinned subtracted two different units and could go negative. That is the sgl-project#715 shape: not a wrong number, but two right numbers that cannot be combined. _scan_existing_files, commit and touch now all charge _allocated_size. reserve deliberately still estimates -- a reservation is taken before the file exists, so the payload length is the only number available -- and commit reconciles it against the filesystem's own answer once the write has landed. RED FIRST, ON AN EVICTION DECISION rather than an accounting field: six tiny pages into a store sized for three ALLOCATED pages. Under apparent accounting the store believes it is at 320 of 1536 bytes and evicts nothing; under allocated accounting it is over cap and must. A test asserting only used_bytes would have passed against an evictor that computed the right number and still evicted on the wrong one. Both halves of max(st_blocks*512, st_size) are covered: ZFS over-allocates small files AND reports st_blocks==1 for large ones under delayed allocation. THE REGRESSION THIS CAUSED, FOUND AND FIXED. The first sweep after the change came back 599 failed against the port-only 590 -- nine new failures, all in test_hicache_file_lru_unit.py, with runtime going 12s -> 235s because the store was over cap from the first write and churning. The cause was the suite's fixtures, not the change: it sizes everything in apparent bytes (max_size="300", three 100-byte tensors, asserting _total_bytes == 300), a premise that collapses once the accounting is correct. B had already solved it, and the filename-grep rule is what found it: the same suite exists in both lineages and B's copy sizes fixtures in a measured _UNIT = 512 with the reasoning inline. Adopted verbatim -- 34 passed in 8.6s, no B-only imports, and identical ruff findings (14 in both A's original and B's copy), so no new lint debt. The other two files in that failure list were checked at both commits rather than assumed collateral: test_swa_eviction_boundary.py 9 failed on both, test_mamba_checkpoint_interval.py 19 failed / 15 passed on both. Standing no-accelerator family. Test results, same selection and env throughout: unmodified base 03adbf8: 590 failed, 180 passed, 485 skipped port only b61d708: 590 failed, 190 passed, 488 skipped this commit: 590 failed, 195 passed, 487 skipped Failures identical to baseline; +15 passed. (881 vs 878 deselected is the three tests in the new suite that the -k filter does not select.) Targeted: 18 passed / 2 skipped across the pin + allocated suites, with the previously skipped 0 != -384 test un-skipped and green and accounting_overshoot_bytes == 0 asserted. A's test_session_checkpoint.py: 48 passed. ruff clean on files I authored. No boots.
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…utover
THE CAUSAL CHAIN, measured 2026-08-17 across two boots:
the carry re-homed a request into running_batch/running_mbs[0] and left
waiting_queue untouched
-> the request existed TWICE: resident (invisible to the policy as
runnable) and queued (counted)
-> _pending_prefill_tokens summed the queue and the resident set without
excluding their intersection, billing the same prompt twice:
51,369 -> 102,307 tokens across one cutover, within rounding of 2x
-> the inflated backlog drove the flip policy past its threshold
-> six cutovers, FLIP-CARRY announcing a resident carry while the policy
read `running bs 0`, the sgl-project#699 detector reporting "1 queued, 0 running",
and the warmup generation never served.
THE FLIP CHURN WAS A SYMPTOM. Six cutovers looked like a flip defect and were
not one: the policy responded correctly to a number that was wrong. Nobody
should "fix" the churn separately.
THREE PARTS, because the defect has three surfaces:
1. STATE -- the carry now CONSUMES what it re-homes
(`_consume_carried_from_waiting_queue`). After a cutover the resident set
owns the request and the queue must not also claim it. Both edges pinned: a
request that is ONLY queued stays untouched, and a carry over an
already-consumed queue is a no-op rather than a corruption. Bookkeeping
never raises into a cutover (sgl-project#715 lesson).
2. NUMBER -- `_pending_prefill_tokens` de-duplicates at the INTERSECTION. Not a
blanket "count each rid once anywhere": a request genuinely holding budget in
two places is a real state a future reader may need to see, and a global
dedup would make that class silent the way this one was. Exactly one overlap
is excluded, and only this one. The sgl-project#713(a) resident term did not create the
duplicate state -- the carry did -- but a counter that sums two sets without
excluding their intersection is the second half, and that half is mine.
3. GUARD -- `duplicate_resident_reqs` gains the waiting queue in its universe.
It compared batches against each other and never consulted the queue, so it
reported "no duplicates" meaning "none of the kind I look for". Queue-side
hits carry a `queued:` prefix so the 2026-08-09 resident-vs-resident specimen
and this one stay distinguishable.
Plus the hardening: `_arriving_prefill_tokens` asserted "ARRIVED but not yet on
the queue" and enforced nothing. Its only inflight-bearing call site is
pre-queue so the invariant holds today, but an asserted-never-checked invariant
is exactly how this double count stayed silent, so it is checked now.
TESTS 18 passed. Mutation-proven THREE ways, each caught: removing the carry's
call site, removing the counter's dedup, and removing the guard's queue
universe.
TWO OF THOSE TESTS WERE VACUOUS FIRST, and both are worth recording:
* the carry test called the helper directly, so deleting the call from
`install_resident_set` left everything green -- the helper worked and nothing
used it. It now drives the real entry point.
* the counter test's duplicate had `extend_range=None`, which the sgl-project#713(a) term
skips by design, so the resident side contributed 0 and the intersection was
never exercised. The duplicate is now made countable on purpose.
Both were found by running the mutations rather than by reading the tests.
REGRESSION managers + mem_cache: 19 failed / 3588 passed against a captured
baseline of 19 -- ZERO new failures. Ruff: scheduler.py 94 errors before and
after, all pre-existing; the other two files clean.
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 17, 2026
…refused on structure Desk-only classification. Verdict and fix SHAPE only -- no fix built; the boot wrapper is F4-r4's and the operator's ack is required. THE HYPOTHESIS DOES NOT HOLD, and it is refused structurally rather than for want of evidence. The store port cannot collide across boots: route_a_631_prod_boot.sh pins neither --nccl-port nor --dist-init-addr, so server_args.py:18787-18788 applies -- nccl_port = get_free_port() draws a port that is free at that moment, and a predecessor still holding its own store port is not a candidate. A lost race is caught anyway at :18873 by wait_port_available, which polls 30 s and names the holding process. There is also a shape argument that does not depend on this codebase: "Connection closed by peer" is a connection ESTABLISHED and then broken, whereas a stale predecessor holding a port yields EADDRINUSE at bind or a refused connect. The observed error is the wrong shape for the hypothesis. So the answer to "does wait_host_release check the store port" is no -- and it should not need to. Extending it there would encode a refuted hypothesis into the boot wrapper, where the next reader would trust it. AT LEAST TWO ROOTS, not one. The specimens split on evidence: - 18:23:43 is not NEAR an OOM event, it IS one. syslog:1789 records "A process of this unit has been killed by the OOM killer" at 18:23:43.842677. The same signature repeats at 18:39:05 against e66bde7's own measurement at 18:39:07 -- F4-r4 called that boot "killed by an external process exit", which is what an OOM kill of a peer looks like from inside a survivor. A third sits at 18:45:30. - 19:31:45 rank2 is NOT memory. The host ledger reads avail=103 headroom=97 at 19:32:24, 39 seconds later, and there is no OOM line anywhere in the 19:3x window. SENDBYTES IS A TOMBSTONE. In 3 of 3 recorded instances it is the SECOND event: HANDOFF_663:696-698 (peer dies, no traceback -- "exactly what a SIGKILL looks like"; that run "died of host RAM"), HANDOFF_658:256 (rank 0's TCPStore died, survivors then spun on sendBytes), and e66bde7's gloo frame naming a dead peer process. Any verdict that makes the socket primary is fighting the prior. THE REAL HOLE is elsewhere and is the recurring shape. wait_host_release.sh computes S -- the count of surviving schedulers -- on every iteration, PRINTS it in the clear-to-boot line, and never puts it in a condition. The gate is available-RAM and nothing else, so a predecessor whose schedulers are alive but whose allocations are already unmapped passes it. The single-instance guard does not cover the gap either: boot script :250 pgreps sglang.launch_server, the LAUNCHER, while the store and ranks live in the sglang::scheduler children -- an orphaned scheduler set whose launcher has exited passes cleanly. That is the counter-without-a-reachable-actuator family (sgl-project#679/sgl-project#681/sgl-project#684/sgl-project#715): the value that answers the question is discarded one line before the decision. FIX SHAPE (not built): gate on the counter that already exists -- require S -eq 0 alongside A -ge NEED -- which closes the predecessor window for RAM, GPU and the store socket at once, without a bespoke port probe. Two cautions handed to the owner: grep -c "sglang::schedul" reads ps comm, truncated at 15 chars, so the match is one rename from silently counting zero and a miscounting gate that reports "clear" is worse than none; and requiring zero turns a soft wait hard, which is the right direction but must fail with PIDs named rather than timing out anonymously. HONEST GAPS, recorded in section 6 rather than papered over: dmesg is permission-denied here and journalctl -k returns "No entries", so kernel OOM detail is UNAVAILABLE from this session, not absent -- which process the killer took at 18:23:43 is therefore not established, only that it fired. The serving tree runs under setsid outside systemd, so the absence of an sglang unit line is not evidence it survived. Specimen B's root and the third specimen's log remain open; a timestamp tied to a specimen is not the same as its log read, and I have not read the second. Adds "schedul" to .codespellrc: it is the literal 15-char truncation ps comm reports, not a typo.
efschu
pushed a commit
to efschu/htsglang
that referenced
this pull request
Aug 26, 2026
…remise POSTEN 1 -- MEASURED FIRST, AND THE PREMISE DOES NOT SURVIVE THE MEASUREMENT. The instruction was: the guard is inert because there is no readable total, so cap `memory.current` against a constant 111.3 GiB from sgl-project#721. Measured on this box before touching anything: pinned_host_memory_bytes() -> total=118.05 GiB available=112.95 GiB so it does NOT land on the :245 "no honest number" branch. The guard is ACTIVE, a total IS readable, and `PINNED_HOST_RESERVE_BYTES` = 10 GiB is therefore NOT inert as a subtraction. The clamp binds too: MemAvailable 113.19 -> 112.95 GiB, reduced by the cgroup's own resident accounting exactly as rule 2 of `honest_host_memory_bytes` documents. `MemAvailable > MemTotal` is FALSE right now, so the pathological lxcfs reading is not present either. AND THE PROPOSED CAP WOULD NOT HAVE BOUND ANYTHING. Every level of the visible hierarchy is unlimited -- root, system.slice and system.slice/claude.service all read `memory.max = max`, and all three report `oom_kill 0` today (sgl-project#721's 17 kills predate this boot; `memory.peak` 101.98 GiB of 118.05). With no enforcing cgroup anywhere, the physical machine IS the ceiling, which is what MemTotal reports. A hand-carried 111.3 GiB constant would have been a third floor with no enforcer behind it -- the rule this ticket already established, applied to its own instruction. Not built. THE REAL DEFECT IS ONE BUCKET OVER, AND IT IS THE sgl-project#695 CLASS AGAIN. Prior art via the new index: commit c043235 measured that "CUDA pinned host memory is accounted in the cgroup's `file` bucket, not `anon` ... the offload ledger reported 20.78 + 14.44 + 14.44 = 49.66 GiB of pinned pool while `anon` sat steady at 14.6 GiB." Confirmed live on this box: anon 2.12 GiB, file 19.06 GiB, current 21.39 GiB -- the clamp charges ~2.4 GiB of a cgroup holding 21.4. `honest_host_memory_bytes` charges anon + kernel + unreclaimable shmem and NEVER charges `file`, correctly, because page cache is reclaimable. Pinned bytes there are not. sgl-project#695 already fixed one member of this class (shmem hiding in `file`); pinned host memory is the second and is still uncounted. IT ALSO PUTS A DOCUMENTED PREMISE IN DOUBT, and the doubt is precise: `pinned_host_budget.py:253` credits already-registered posts BACK to available because "their bytes are therefore already missing from it". For a post whose bytes land in `file`, they are NOT missing from it -- the clamp never subtracted them -- so the credit-back would count them as free twice. The 2026-08-17 measurement quoted there is sound for the weight images it was taken against; whether it holds for THIS pin depends on which bucket the pin's bytes land in. DELIBERATELY NOT FIXED HERE. That question is decidable only by watching `anon` and `file` across an actual pin allocation, which is a boot. Changing admission arithmetic on a shared path used by HiCache and kv-session-offload, on a guess about which bucket, would be the "capping on a fabricated figure" the module exists to prevent. So it is INSTRUMENTED instead -- the window script prints the before/after pair and names the consequence -- and the arithmetic is left alone until the boot answers it. POSTEN 2 -- `chunk_blocks_quiescence` (phase_flip_runtime.py:144): SAME CLOCK DIVERGENCE, and the determination is not a formality. It is the same class as the retention gap: a consumer (the cutover) and a writer (the chunk boundary that inserts into the tree) on different clocks. Under STRICT the clocks are SYNCHRONISED -- the cutover waits for the prefill, which is what "waiting for prefill to finish IS drain-and-flip" means in its docstring -- and the residual is exactly the NON-STRICT case where they are not. THE DIFFERENCE THAT MATTERS, and why this is one root and not two: the chunk case has a BOUNDED wait available (a prefill completes in finitely many chunks), while a mid-decode resident does not. That is why STRICT can fix one and nothing can fix the other by waiting. But the NON-STRICT chunk case cannot be fixed by waiting either -- an unconditional block re-creates sgl-project#631 defect O, the 32768-token prefill that ran in the slow layout and paid two cutovers for nothing. So BOTH halves need the same missing capability: persisting PARTIAL work at the seam. Neither is closable without it, and that capability is `seam_copy_state` / `restore_seam_state` (schedule_batch.py:2054/:2089) -- sgl-project#875's active territory. Reported, not touched, per Posten 4. Unlike the sgl-project#813/sgl-project#852/sgl-project#715 convergence I rejected last round, this one IS a shared root, and I am saying so because the evidence supports it, not because it tidies. POSTEN 3 -- `scripts/window_871a_verify.py`: one call, PASS/FAIL with numbers. Decides all three open claims from a boot log plus the cgroup. Exit 0 all passed, 1 a decided negative, 2 UNDECIDED -- and 2 is never a pass: "the evidence was not there" is a different fact from "the claim is false", and it sends the reader back to the boot. It boots nothing, claims no card, restores no serving. It also prints the sgl-project#721 HOST-LEDGER pair (posts + memory.current / peak / anon / file, before vs after) that Posten 1 needs. IT SHIPPED A FALSE PASS AND I CAUGHT IT BEFORE THE WINDOW, which is the whole argument for mock-smoking a window script. The first version summed `acked=` over the WHOLE log and returned PASS on the W40 boot -- the very boot in which all 21 fences reported `acked=0`. Seven lines there carry `acked=` from an unrelated subsystem, three of them `acked=24`, so it credited 72 acknowledgements no fence ever made. A window script that reports a false PASS is worse than none: it CLOSES an open claim. Now scoped to the fence's own lines, with that exact log shape as a permanent self-test case. The strongest desk check available is wired in: run against the real W40 log it must reproduce that boot's known split -- tier armed PASS, store delivery FAIL, exit 1 -- and it does. TESTS (hermetic; no boot; no card touched). CUDA_VISIBLE_DEVICES="" verified at the process and `nvidia-smi --query-compute-apps` empty throughout; GPUs 0/0/0. Runner reported its exclusions. * the script's own --self-test decides SIX cases in the intended direction, both polarities, including the false-PASS regression. * can-fail by mutation: un-scoping the acked regex reddens the real-boot reproduction and the self-test (2 red); restored green. * A SECOND SELF-INFLICTED DEFECT FOUND AND FIXED: the test located the script by counting `dirname` calls, landed on `test/` instead of the repo root, and failed all six cases -- a green arm red for a reason unrelated to the thing under test. It now walks up to the root. * partitioned tier-2 gate: 0 failing, 4421 passed (wide 3775 / narrow 292 / serial 354). Serial +6 over 348 is exactly the six tests added here. names=0 agrees with the summary on every lane. * test/registered/unit/mem_cache: 2 failed / 1802 passed -- the known pre-existing test_acceptance_emitters_758 RefillTiming pair, unchanged. POSTEN 4 -- boundary held. Nothing in schedule_batch.py, phase_flip_spill.py or seam_kv_recover was read into or written. Both Posten 2's root and A's remedy lead there; both are reported rather than resolved. NOT ESTABLISHED. Which cgroup bucket the phase-flip pin's bytes land in -- the question Posten 1 turns on -- needs the boot. Whether the admission ever refuses on this box, likewise. And sgl-project#721's 17 oom_kills are from a previous boot: today's counters are zero at every level, so nothing here re-measures that incident, it only records that the enforcement it implies is absent.
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.
Thank you for your contribution, we really appreciate it. The following instructions will help improve your pull request and make it easier to receive feedback. If there are any items you don't understand, don't worry. Just submit the pull request and ask the maintainers for help.
Motivation
Please explain the motivation behind this PR and the goal you aim to achieve with it.
Modification
Briefly describe the changes made in this PR.
Checklist
pre-commit run --all-filesor other linting tools are used to fix potential lint issues.