Skip to content

docs: update README - #688

Merged
zhyncs merged 1 commit into
sgl-project:mainfrom
zhyncs:fix
Jul 21, 2024
Merged

zhyncs merged 1 commit into
sgl-project:mainfrom
zhyncs:fix

Conversation

@zhyncs

@zhyncs zhyncs commented Jul 21, 2024

Copy link
Copy Markdown
Contributor

Motivation

cc @merrymercy @Ying1123 @hnyls2002

fix

ERROR: Project file:///home/ubuntu/sglang/python has a 'pyproject.toml' and its build backend is missing the 'build_editable' hook. Since it does not have a 'setup.py' nor a 'setup.cfg', it cannot be installed in editable mode. Consider using a build backend that supports PEP 660.

on Lambda GPU

Modification

as titled

Checklist

  1. Ensure pre-commit pre-commit run --all-files or other linting tools are used to fix potential lint issues.
  2. Confirm that modifications are covered by complete unit tests. If not, please add more unit tests for correctness.
  3. Modify documentation as needed, such as docstrings or example tutorials.

@zhyncs
zhyncs merged commit 9dab947 into sgl-project:main Jul 21, 2024
@zhyncs
zhyncs deleted the fix branch July 21, 2024 08:33
timethink pushed a commit to timethink/sglang that referenced this pull request Mar 9, 2025
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 16, 2026
…ed inert under load

TWO FINDINGS FROM THE LOADED CORRIDOR RUN, one a fix and one a negative
result that stops a default from moving.

1. A RUNG THAT RETURNS NOTHING NOW SAYS SO.

The pre-gate KV relief was logged only under `if kv_freed > 0`. On 2026-08-16
the seam was refused 76 times from 06:47:48 with no relief line anywhere in
the log, so "the rung returned 0" and "the rung never ran" were
indistinguishable -- and the guard's own "reclaimed 0 MiB from [nothing]" was
then read (by me, in the sgl-project#687 diagnosis) as the rung being exhausted.

IT NEVER SAID THAT. That string is the GUARD LADDER's provider list, and the
ladder has exactly two providers, allocator-cache and draft-weights. No KV
provider is registered with the guard anywhere in the tree, deliberately: the
cap is a group decision and the ladder is rank-local, so the rung's bytes
arrive as `kv_freed` BEFORE the guard probes and can never appear in that list
however much it paid. The zero line now says that outright, and names the
three non-interchangeable causes to check: the admission floor (healthy), an
empty evictable set (the pool is genuinely live), a disqualified rung (a
defect).

2. SGLANG_FLIP_SEAM_CHUNK_MIB=16 BUYS NOTHING UNDER LOAD. DEFAULT STAYS 0.

Row-blocking's own comment required a loaded corridor run before its default
could move, because its measured 488.7 -> 305.6 -> 276.5 MiB shrink at
B = 1, 4, 16 "was taken at 90 live slots and prices the seam's constant, not
its behaviour under a full pool". That caution was right.

Two boots of 8974994, same argv, same 4x25625-token load, one variable:

  chunk OFF   185650 live slots   staging 1456.14 / 1354.00 / 2016.64 MiB
  chunk 16    288904 live slots   staging 1657.42 / 1555.57 / 2250.81 MiB

Fitting staging = a + b*slots per rank across each boot's own points:

  slope      0.001953 (off)  vs  0.002064 (armed)  MiB/slot
  intercept  1093.6 / 991.4 / 1686.6   vs   1061.3 / 959.4 / 1654.6 MiB

Identical within 3%. The knob was verified live in the worker environment,
not merely exported. The shrink does not appear because the backing term is
`max(ceil(max(com_w, B*com_w - (B-1)*rel_w)/B), min(com_w, chunk_floor))`:
with a release leg small against the commit leg the numerator stays near
B*com_w and the division cancels, and the arena chunk floor binds what is
left. At 90 live slots those terms sit differently, which is precisely why
the comment demanded this run.

Recorded rather than deleted: the knob and its 16-block default remain, so a
successor with a different pool geometry can re-price it without rebuilding
the machinery. What must not happen is shipping it as a default on the
strength of a 90-slot measurement.

BOTH BOOTS SERVED CLEANLY on the sgl-project#687 law-advisory code: 12 and 15 flips,
0 refusals, 0 delays, 0 withholds, 0 drain-mode yields, 4/4 requests each.
The law warning fired 2 and 3 times and the seam proceeded every time.

TESTS: 2 new source-assertion cases pinning the zero-report branch and the
clause that ends the guard-ladder confusion (the same convention this file
already uses for the rung's wiring), 29 in that file, 974 across every
corridor / phase / purity / seam / margin / kv_backing suite.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 16, 2026
…ed inert under load

TWO FINDINGS FROM THE LOADED CORRIDOR RUN, one a fix and one a negative
result that stops a default from moving.

1. A RUNG THAT RETURNS NOTHING NOW SAYS SO.

The pre-gate KV relief was logged only under `if kv_freed > 0`. On 2026-08-16
the seam was refused 76 times from 06:47:48 with no relief line anywhere in
the log, so "the rung returned 0" and "the rung never ran" were
indistinguishable -- and the guard's own "reclaimed 0 MiB from [nothing]" was
then read (by me, in the sgl-project#687 diagnosis) as the rung being exhausted.

IT NEVER SAID THAT. That string is the GUARD LADDER's provider list, and the
ladder has exactly two providers, allocator-cache and draft-weights. No KV
provider is registered with the guard anywhere in the tree, deliberately: the
cap is a group decision and the ladder is rank-local, so the rung's bytes
arrive as `kv_freed` BEFORE the guard probes and can never appear in that list
however much it paid. The zero line now says that outright, and names the
three non-interchangeable causes to check: the admission floor (healthy), an
empty evictable set (the pool is genuinely live), a disqualified rung (a
defect).

2. SGLANG_FLIP_SEAM_CHUNK_MIB=16 BUYS NOTHING UNDER LOAD. DEFAULT STAYS 0.

Row-blocking's own comment required a loaded corridor run before its default
could move, because its measured 488.7 -> 305.6 -> 276.5 MiB shrink at
B = 1, 4, 16 "was taken at 90 live slots and prices the seam's constant, not
its behaviour under a full pool". That caution was right.

Two boots of 8974994, same argv, same 4x25625-token load, one variable:

  chunk OFF   185650 live slots   staging 1456.14 / 1354.00 / 2016.64 MiB
  chunk 16    288904 live slots   staging 1657.42 / 1555.57 / 2250.81 MiB

Fitting staging = a + b*slots per rank across each boot's own points:

  slope      0.001953 (off)  vs  0.002064 (armed)  MiB/slot
  intercept  1093.6 / 991.4 / 1686.6   vs   1061.3 / 959.4 / 1654.6 MiB

Identical within 3%. The knob was verified live in the worker environment,
not merely exported. The shrink does not appear because the backing term is
`max(ceil(max(com_w, B*com_w - (B-1)*rel_w)/B), min(com_w, chunk_floor))`:
with a release leg small against the commit leg the numerator stays near
B*com_w and the division cancels, and the arena chunk floor binds what is
left. At 90 live slots those terms sit differently, which is precisely why
the comment demanded this run.

Recorded rather than deleted: the knob and its 16-block default remain, so a
successor with a different pool geometry can re-price it without rebuilding
the machinery. What must not happen is shipping it as a default on the
strength of a 90-slot measurement.

BOTH BOOTS SERVED CLEANLY on the sgl-project#687 law-advisory code: 12 and 15 flips,
0 refusals, 0 delays, 0 withholds, 0 drain-mode yields, 4/4 requests each.
The law warning fired 2 and 3 times and the seam proceeded every time.

TESTS: 2 new source-assertion cases pinning the zero-report branch and the
clause that ends the guard-ladder confusion (the same convention this file
already uses for the rung's wiring), 29 in that file, 974 across every
corridor / phase / purity / seam / margin / kv_backing suite.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 16, 2026
…s standing in front of

54 MINUTES OF SILENT OUTAGE, health 200 throughout. From 16:23:10, 325
consecutive lines of:

    BOTH BLOCKED: nothing can run in the pp layout and the target cannot admit
    either (0 req resident, 10495392 tok pending)

Last real batch 16:23:11: full token usage 1.00, #running-req 0, mamba 0.17.
The whole KV pool was radix cache with ZERO resident requests -- every row
unlocked and evictable -- while 10.5M tokens queued and three GPUs sat at 0%.

ONE SWALLOWED EXCEPTION. _post_evict_rows asked tree_cache.evictable_size().
MambaRadixCache does not return a number from that method; it raises
NotImplementedError and says "use full_evictable_size() and
mamba_evictable_size() instead". The probe caught it and used 0, so on the
class this rig runs it returned `available` ALONE -- the exact error its own
docstring warns about, committed three lines below the warning.

At usage 1.00 that reads ~0, so every admissibility question answered no:
pp could not admit, tp had nothing resident to decode, and sgl-project#688's BOTH BLOCKED
branch declined. That branch returns BEFORE alloc_token_slots -- so the
allocator was never reached, eviction never ran, and the unlocked cache was
never freed. The receipt called it "an evict trigger" while no evict could
occur, which is the same counter-vs-actuator shape as sgl-project#681/sgl-project#694 in a third
place: a message naming an action nothing performs.

CONFIRMED BY ABSENCE in the specimen (WEDGE-2026-08-16T1623Z.txt): zero RADIX
SHAPE, zero "Out of memory", zero EVICTION UNDER-DELIVERED. The allocation path
was never entered. py-spy shows the ranks spinning the event loop building
nothing.

THE TRAP IS ALREADY DOCUMENTED IN THIS TREE, at common.py:411-425, for these
same two classes -- and I read that comment the same day while diagnosing sgl-project#694
and did not apply it here. The resolution order is now COPIED from there rather
than re-derived, because two spellings of one rule is how this comes back.

WHY NOW. The bug shipped with sgl-project#688's admissibility simulation and needed
usage == 1.00 to bite. sgl-project#696's floor repair shrank the pool by 39,504 tokens, so
full occupancy arrived sooner and the wedge began 12 minutes after that boot.
sgl-project#696 EXPOSED this; it did not cause it.

THE SHAPE TO LEARN: a swallowed exception that yields a PLAUSIBLE value. Zero
is a legal row count, so nothing downstream could tell "the cache holds
nothing" from "the cache was never asked". Every accessor is now tried in turn
and only a genuine absence of all of them yields zero.

Health being 200 for the entire outage is the second lesson: the endpoint
answers while the scheduler builds no batch. It is not a liveness signal.

TESTS: test_post_evict_rows_698.py 5 passed (red-first: pre-fix it fails
"0 not greater than or equal to 150000", reproducing the wedge arithmetic).
managers + mem_cache 64 failed / 1922 passed -- the same 64 baseline.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 16, 2026
…es, and pin it

THE INVARIANT WAS PROSE. phase_policy's branch says "Declining here is what
routes the caller to the evict rung instead of to a cutover." The caller did
not: scheduler.py handled every decline identically -- one throttled log line,
return. So on 2026-08-16 the instance printed "this is an evict trigger and NOT
a flip" 350 times across 54 minutes while no eviction was ever attempted,
health answered 200 the whole time, three GPUs sat at 0%, and 10.5M tokens
queued behind a pool nothing would free.

I wrote that comment in sgl-project#688. It is the fourth counter-vs-actuator member found
today, and the first where the actuator existed only as a sentence.

WHAT THIS COMMIT DOES: the decline now calls evict_from_tree_cache, bounded to
one attempt per 5 s (the decline is evaluated every round, so an unbounded call
walks the whole tree in a tight loop on an already-wedged box), and REPORTS
WHAT EVICTION RETURNED.

THE REPORT IS THE POINT AS MUCH AS THE CALL. "The remedy ran and freed 0" and
"the remedy never ran" are the two states this outage could not distinguish,
and telling them apart is what turns a silent wedge into a diagnosis.

AND ON THIS SPECIMEN IT WOULD HAVE DELIVERED ZERO -- said plainly, because the
routing is necessary and NOT sufficient. At 16:22:58-16:23:11 a single request
was mid CHUNK-PREFILL (#new-seq 1, #new-token 512, #cached-token 0) while usage
climbed 0.95 -> 1.00, with no retract, abort or finish anywhere in the window.
A chunked request is resident but sits in NO BATCH (sgl-project#631 defect O), which is
why the scheduler read #running-req: 0 while its own protected prefix held the
pool. Eviction cannot free a locked chain. The deeper root is that a chunked
request's prefix can fill the pool its next chunk must allocate from -- a
self-deadlock -- and that is a separate fix on the chunked/admission path, not
this one.

Also exports BOTH_BLOCKED as a shared constant instead of matching a respelled
literal across two modules.

TESTS: test_both_blocked_routing_698.py 6 passed -- the sgl-project#505 discipline: the
pin fails if the caller stops routing, if the relief stops calling eviction, if
it stops reporting a zero delivery, or if the rate limit is removed.
managers + mem_cache 64 failed / 1928 passed, the same 64 baseline.
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
…ead them the same

_resident_ceiling returns -1 for two OPPOSITE states, and its own docstring
says so: "Highest row a RESIDENT REQUEST pins, or -1 when none/unknown."
build_flip_live_slots_fn sets req_max to -1 when it has no request parts, so an
IDLE box is encoded exactly like an UNREADABLE split.

The two demand opposite behaviour:
  * unreadable    -> evict NOTHING. Unmapping a row a live request is reading
    is the one unrecoverable error, so the conservative branch is right here.
  * none resident -> the entire live set is tree-only and MAXIMALLY evictable.

_evict_floor_rows took the conservative branch for both, which means it refused
to price an eviction precisely when it had the most to win. On the specimen:

    BOTH BLOCKED ... 0 req resident
    KV rung: current=137216 rows, floor=398471, slack=0

397,958 max_live against a 137,216 cap -- 2.9x over -- so slack pins to 0, the
sgl-project#688 evict-rung funding path is structurally dead, and every flip falls back on
the raw seam budget. That is why three FLIP ABANDONED events turned on a mere
55 MiB shortfall.

ROOT DIFFERS FROM THE FILED HYPOTHESIS, and the distinction matters for the
neighbouring tasks: the ids are NOT stale pre-shrink survivors needing a
rebase. 397,958 is a perfectly valid id in the ~437k id space; the pool simply
has only 137,216 rows BACKED. The floor is max(live id) + reserve, a SPARSE
high-water mark, so ~137k live rows scattered across the id space demand
398,471 rows of backing. Nothing is un-rebased; the mark is just the wrong
statistic when the live set is sparse. A rebase would not have fixed this.

The two states are separable from data already recorded -- the live-set split
carries req_rows beside req_max -- so no new enumeration is needed.

Tests, hermetic (CUDA_VISIBLE_DEVICES=""):
  test_the_specimen_shape_reproduces ......... the plain floor IS 398471, and
      it exceeds the 137216 cap, so slack pins to 0
  test_nothing_resident_prices_the_eviction .. the fix: floor collapses under
      the cap and an eviction is priced
  test_unreadable_split_still_refuses ........ CAN-FAIL: the conservative
      branch must survive for its real case. A fix that treated -1 as "evict
      everything" passes the fix test and fails this one -- and that mistake
      unmaps live rows.
  test_resident_rows_present_is_unchanged, test_nothing_resident_predicate_is_exact
  -> 5 passed + 3 subtests; 76 passed + 5 subtests across 717 + flip-runtime +
     715 suites. ruff clean in the changed region.

CAN-FAIL PROVEN BY MUTATION: restoring the conflation fails 1 of 5 (the fix
test), reverted and re-verified 5 passed.

Desk only -- not yet observed on metal. The live proof is a flip funded by the
evict rung on an idle box, which the next boot can show.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 17, 2026
…e rules outrank it

Wiring for 59592d6, plus both review findings, plus three precedences the
EXISTING TESTS taught me -- which is the substance of this commit.

FINDING 1, THE TP MIRROR. The tp branch pulled unconditionally on pend>0, so
after an EXHAUSTED release the next TP evaluation would pull straight back and
decode would lose the layout before serving anything: the C2 defect with the
phases swapped, degenerating under both-sides load to max_hold PP rounds, ~0 TP
rounds and TWO seams per cycle -- strictly worse than the timer it replaced.
The pull now yields to a RUNNING decode batch for MIN_DECODE_ROUNDS (2: the
minimum that means "did not preempt mid-batch"), bounded so it cannot become a
new starvation of the prefill side. The degenerate cycle is pinned by test, not
by hope: release at the bound, then assert the pull does NOT come straight back.

FINDING 2, THE COUNTER LIFECYCLE. hold_rounds_so_far was caller-maintained.
next_hold_rounds() is now a pure function resetting on EITHER boundary -- phase
change or pending reaching 0 -- and the defect it prevents is pinned directly:
a stale counter of 8 carried across a phase change releases the very first hold
of the next episode, i.e. the C2 fix evaporates for the arrival that needed it.

FINDING 3, THE REASONS REACH THE LOG. The gate returns its verdict as the
decision's reason, so it travels the same path BOTH BLOCKED already does -- that
is how the boot's acceptance gets read.

THE THREE PRECEDENCES, each taught by a test I broke:
  * sgl-project#688 idle-locked outranks it, exactly as it outranks sgl-project#689 formation:
    holding a layout that can build NOTHING is waiting inside a layout that
    cannot serve.
  * The DRAINED exit outranks it on sgl-project#669's economics: a residual ABOVE one
    chunk stays (which this hold wants anyway), but a SUB-CHUNK residual is
    finishing regardless and holding for it spends a ~2.6 s seam to save a
    fraction of a chunk. sgl-project#669 moved anti-pinning to the starvation cap
    precisely so the drain could exit.
  * The DECODE STARVATION CAP outranks it absolutely. That SLO is the system's
    guarantee that PP can never pin the server; this lever's round bound is
    only a secondary backstop. Vetoing the cap would put a local timer above a
    global guarantee and reintroduce the pinning it prevents.

AND THE GATE IS PP_TO_TP ONLY, as sgl-project#689's is. My first wiring vetoed EVERY arm,
including the idle return leg -- 11 tests red -- because the rules arm for
reasons this lever cannot see. It may only convert the one arm that contradicts
it. Recorded because three exemptions is a shape: if a fourth appears, invert to
an allowlist rather than adding it.

Tests: 22 passed + 17 subtests in the lever file; 221 passed + 24 subtests
across phase-policy, idle-locked-arm, window-formation, admission-intake, 708
and flip-runtime. ruff clean.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 17, 2026
…-pong

A layout may be declared unable to run only after it has HAD THE CHANCE to
run. An emptiness observed within idle_locked_settle_s of entering a layout is
a transient, not a verdict.

THE CLAIM THAT FAILED

The IDLE_LOCKED branch sits above the min-dwell check and bypasses the only
anti-thrash bound, justified by a comment stating "IT CANNOT OSCILLATE ...
after the flip the target runs by premise, so the same condition is false
there". It oscillated. Across the 16 boot rotations of 2026-08-17 the policy
produced alternating runs of 72 arms / 299 s, 12 / 31 s and 10 / 27 s twice.

The premise fails on WHEN it is evaluated, not on what it says. This branch is
reached on the first round after a cutover, while the just-entered layout is
still empty and its carried work not yet re-admitted --
Scheduler._idle_locked_inputs is gated on _round_built_nothing, which a
just-entered layout satisfies trivially. The new layout is observed in its
empty transient, certified unable to run, and armed straight back.

That is also where sgl-project#713's TTFT quantisation came from. The unit of delay is one
whole cutover (median 2864 ms tp_to_pp, 2772 ms pp_to_tp over 486 flips), so
the 0.1 / 3.1 / 5.9 s levels in the sgl-project#713 tables are how many cutovers a request
sat through. The 06:19 table was taken during a 27 s run.

THE VALUE IS DERIVED, NOT CHOSEN

Delay from "cutover complete" to the first batch the new layout builds, over
162 cutovers: 0 s for 66 of 150, 1 s for 34, 2 s for 27, 3 s for 12, thin tail
to 6 s, one outlier at 30 s. 2.0 s covers 84.7 %. The p95 (4 s) is deliberately
NOT used: it exceeds the 3 s min_dwell_s this rig boots with, and a settle above
the dwell would make the fast escape slower than the path it exists to bypass.
Log stamps are second-resolution, so this is a bound, not an optimum, and the
docstring says so.

FAILURE DIRECTION, AND WHAT BOUNDS IT

A genuine idle lock forming within the settle of a cutover is now delayed, by at
most the settle. Bounded three ways: the value is capped at min_dwell_s at the
use site, so an operator cannot configure the fast escape into being the slow
one; the 180 s decode-stall cap remains the backstop that released the 09:42:39
specimen; and the settle is consulted only right after a cutover, which is the
only place the transient exists. sgl-project#688's escape is preserved and pinned.

phase_since, not last_flip_at: the first is when THIS layout was entered and is
maintained from the OBSERVED phase, so a manual POST /phase_flip restarts it
too; the second is an arm stamp that is 0 until the policy has armed once. None
degrades to pre-guard behaviour rather than to an infinite settle.

RED-FIRST, AGAINST THE RECORDING

test_idle_locked_settle_713.py replays the recorded arms through the real policy
off scripts/fixtures/d2_injector_pingpong_excerpt.txt, reusing the injector's
parser so harness and suite cannot disagree about what the log says.

  settle disabled -> 12 of 12 arms armed, the ping-pong reproduced
  settle enabled  -> 1 of 12 armed

Not zero, and that is the point: the arm that legitimately leaves a 35 s-settled
idle-locked layout survives, and only the eleven arm-backs on 0-1 s-old layouts
are refused. The first version of this replay asserted all twelve were
transients; the data refused, one arm being 35 s past its cutover. That arm was
not noise, it was the control case, and it is now the selectivity pin.

Mutation-proven: disabling the guard at source turns 3 tests red.

REGRESSIONS  Baselined before and after by capturing the failure set with the
guard reverted. 8 failures pre-exist on this branch in this area and are
untouched; the guard adds none and 705 pass. The five sgl-project#677-era pins stay green
because they construct PhasePolicyState() with no phase_since -- which is also
why they never caught this, and their docstring now says so instead of
restating the falsified invariant.

sgl-project#712 TEXT REMOVED  The BOTH-BLOCKED decline used to redirect to "the state-slot
bound (mamba/GDN slots)". That was never measured: it was a hypothesis authored
into a log string, read back out of the log and filed as a finding, and sgl-project#712 was
closed as unfounded on that evidence. The line now reports only what it knows.
test_both_blocked_binding_resource_708 REQUIRED that wording, so the pin is
inverted rather than relaxed -- an unmeasured cause must not be named.
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 17, 2026
Fix for the root filed in ANALYSE_741. On 2026-08-17 19:30:40 the KV backing
rung evicted 127,731 rows that the phase flip was about to pack, and the next
access above the new cap was an illegal memory access 24 log lines later. It
killed that day's serving.

WHY BOTH HALVES WERE BLIND AT ONCE, which is what shapes the fix.
_nothing_resident() asks the live split for req_rows, and _shrink_to's safety
net re-measures through _max_live_row() -- which calls the SAME
_live_slots_fn. A flip quiesces its requests before packing them, and a
quiesced request sits in none of the batch structures _live_reqs enumerates,
so that one enumeration reported zero to both. Fixing only the predicate would
have left the net equally blind to any other caller that shrinks during a
park, so both are fed from ONE side channel.

LINE 1 -- the parked extent is visible to predicate AND net.
_nothing_resident() consults _flip_pending() first; _max_live_row() folds the
parked max row id in, which is the half a predicate-only fix would have
missed. (-1, -1) means UNKNOWN and blocks in both -- never reads as empty.

LINE 2 -- the rung refuses outright while a flip is armed, on the pricing and
the collecting site both, because the failure is a SILENT eviction followed by
a DELAYED fault and one mechanism is not enough. Gated on ARMED ONLY: outside
a flip the rung stays fully live, which sgl-project#688's evict-rung funding path depends
on.

WIRED, not merely defined. Both channels are passed at the production factory
against scheduler.phase_flip_runtime, and an AST pin asserts the factory
actually passes them -- parsed rather than grepped, because the module's own
prose names both kwargs while explaining them. A fix wired into nothing is the
defect class this repo keeps finding; it is not shipping as one here.

TEST RESULTS (repo rule: documented before commit)

New suite test_evict_rung_flip_park_744.py: 15 tests + 2 subtests, all green.
Can-fail proofs cut BOTH ways on purpose -- removing the extent visibility must
let the eviction happen again, and the gate must NOT make the rung dead
outside flips.

Mutation matrix, 6 mutants, ALL KILLED (run against the new suite plus both
sgl-project#717 suites):
  M1 predicate ignores parked extent          -> 3 failed
  M2 net ignores parked extent                -> 1 failed
  M3 gate removed from pricing site           -> 1 failed
  M4 gate removed from collecting site        -> 2 failed
  M5 gate ALWAYS on (rung dead outside flips) -> 3 failed   <- the sgl-project#688 guard
  M6 unknown extent treated as empty          -> 1 failed
M5 is the one that matters most: it proves the gate is not permitted to
disable the rung outside a flip.

Pinned suites green: test_evict_rung_nothing_resident_717,
test_evict_rung_floor_invariant_717, test_kv_backing_cap_agreement_656,
test_kv_backing_recovery_clamp_684, test_kv_backing_exhaustion_662_f4 --
88 passed with the new suite.

Full managers regression, compared PER SUITE against HEAD in a scratch
worktree (per the truncated-baseline lesson -- a total can hide a swap):
  baseline HEAD : 29 failed, 4 errors, 2522 passed
  with sgl-project#744     : 28 failed, 4 errors, 2540 passed
  NEW failures  : ZERO
The single baseline failure absent from my run
(test_phase_flip_mover_streaming_631 :: test_the_waved_price_is_short_of_the_
measured_live_set) is NOT claimed as a fix: it passes 3/3 in isolation on BOTH
trees, so it is order-dependent in the full run and unrelated to this change.

Ruff and codespell clean.

Two things stated plainly rather than dressed up. The fix was built BEFORE the
falsifier, contrary to red-first; the mutation matrix is the equivalent
evidence and the ordering was mine. And the sgl-project#717 suites caught a real error at
once -- they invoke the rung's methods UNBOUND against stubs, my first version
used direct attribute access, and all four went red; fixed by matching the
file's existing getattr(self, ..., None) idiom.

KNOWN LIMITATION, filed as its own follow-up: _flip_pending() returns the last
enumeration that saw requests, consulted only while armed. That makes line 1
CONSERVATIVE rather than exact -- a flip that arms before any enumeration has
seen requests answers UNKNOWN and blocks. Safe, but the exact arm-time
snapshot is not shipped here.

No boot. Boot proof goes on F4-r4's window list.
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.

1 participant