Skip to content

docs: update README - #935

Merged
zhyncs merged 1 commit into
sgl-project:mainfrom
zhyncs:new
Aug 5, 2024
Merged

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

Conversation

@zhyncs

@zhyncs zhyncs commented Aug 5, 2024

Copy link
Copy Markdown
Contributor

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

  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 fde8340 into sgl-project:main Aug 5, 2024
@zhyncs
zhyncs deleted the new branch August 5, 2024 10:06
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 27, 2026
…spec in sgl-project#936

NOT A FIX. This adds one function and calls it from nowhere. It is committed
alone so it cannot be lost in a worktree, and so the successor building sgl-project#936
starts from a written rule rather than from a description of one.

WHAT IT IS. `lower_protected_to(req, new_protected_len)` lowers
`req.cache_protected_len` by `min`, never assigns and never raises it.
`cache_protected_len` means "the tree owns the KV below this" -- the premise
both free-sites in `cache_finished_req` rest on and the one
`retention_shrinks_protected` states in this same module. Raising it would
promise ownership nobody holds; a flat assignment would clobber a HIGHER claim
a later legitimate match had earned. So: min.

WHY IT EXISTS. When the tree does NOT take a request's prefix, the promise is
false for the rows that request still holds, and the two branches that free
FROM `cache_protected_len` while inserting nothing then skip over them:

  * `unified_radix_cache.cache_unfinished_req`, the `effective_cache_len <= 0`
    skip branch -- frees `kv_indices[req.cache_protected_len:]` (:1196) and
    returns (:1212) without inserting and without lowering cpl;
  * `unified_radix_cache.cache_finished_req`, the `is_insert=False` branch
    (:1130-1131) -- identical shape.

The `effective_cache_len <= 0` that opens the first one comes from mamba's
`_decline_retention(is_finished=False)`, which returns 0. That refusal is
measured, not inferred -- boot 2g, in its own words: "mamba retention would
truncate a protected prefix: tracked position 4096 under cache_protected_len
8192, caching without a mamba anchor".

THE RULE sgl-project#936 SHOULD APPLY: if the tree does not take the prefix, there is
nothing left to protect -- cpl falls in the SAME moment to what the tree
actually holds (0 when nothing was inserted), and the free then covers
everything. At the moment of the refusal, never blanket on the request: a
later successful match may legitimately grant a protected prefix again.

EXPECTED TO BE REPLACED. sgl-project#930's `truncate_prefix_to` carries the same
min-never-assign discipline; when it lands, sgl-project#936 should use it and delete this
one. Two helpers for one rule is the shape that lets them drift.

sgl-project#935's guard (fa7dd3b) is the falsifier for that work and stays as it is: it
MEASURES the orphaned interval rather than repairing it, so if it still fires
after sgl-project#936 there is a second source and it names the share.

Desk checks only, as befits an unwired helper: py_compile clean, ruff clean.
No test, because a function with no caller has no behaviour to pin -- the
red-first test belongs with the wiring, and its shape is specified in sgl-project#936
(mamba retention refused + a full cache_protected_len, asserting the interval
is neither inserted nor freed today).
efschu pushed a commit to efschu/htsglang that referenced this pull request Aug 27, 2026
…ted_len with it

`cache_protected_len` is HOW MANY LEADING ROWS OF THIS REQUEST'S KV THE TREE
OWNS. `init_next_round_input` sets it equal to `len(prefix_indices)`. The sgl-project#791
admission-uniformity block in `_get_new_batch_prefill_raw` then truncates
`prefix_indices` to the PP-agreed `told` -- on PP0 from the guard's clamped
candidate, downstream from PP0's decision -- and NEITHER branch touched the
protected length. The request was left claiming more tree-owned rows than it
holds.

I FILED THIS AS "THE SAFE DIRECTION" AND THAT WAS THE WRONG HALF. The surplus
IS harmless for `_insert_helper`'s duplicate free -- a larger `dup_start` frees
less -- which is exactly why it never surfaced as a double claim. It is the
DANGEROUS direction for `cache_finished_req`'s truncate branch
(`unified_radix_cache.py:1111-1116`):

    free_start = max(effective_cache_len, req.cache_protected_len)
    free(kv_indices[free_start:])    # starts ABOVE the interval
    ...                              # the insert covers only up to ecl

With `cache_protected_len > effective_cache_len` the rows in
`[effective_cache_len, cache_protected_len)` are neither freed nor inserted and
belong to nobody afterwards. That interval is sgl-project#935's per-request row leak
(36824 rows on the 2i acceptance boot).

SCOPE, kept sharp so the two tickets do not blur. The GAP is the root and is
sgl-project#935's: it must not be able to leak whatever the value is. This closes one of
the two PRODUCERS that make it reachable; the other is the sgl-project#928 refusal
re-prefill. Closing a producer does not close the gap, and closing the gap
makes the producers harmless -- both are owed, and neither substitutes for the
other. Different files, no collision with fix/935-finished-req-gap.

ONE HELPER, TWO SITES, because the two sites are siblings of each other and
drifted identically -- both sliced `prefix_indices` by hand and both forgot the
same field. `Req.truncate_prefix_to(told)` now owns the pair, and the wiring
pin asserts neither branch slices by hand again.

MIN, NEVER ASSIGN: the helper may only LOWER the claim. A request whose
protected length was already below `told` owns exactly that many, and raising
it here would invent protection the tree never granted -- which is the
dangerous direction for the duplicate free, i.e. the defect this commit is
NOT allowed to trade for. Pinned in both directions.

Tests, hermetic, CUDA_VISIBLE_DEVICES="":
test_truncation_keeps_protected_len_930.py, 6 passed. Combined
mem_cache+managers gate: 17 failed / 7483 passed, the 17 NAME-IDENTICAL to
f1a3391's -- zero new failures. Genuinely red-first with
the mutant the ticket names -- restoring "slice the prefix, never touch cpl"
turns 3 red, including the consequence test that computes the abandoned
interval from the real `max(ecl, cpl)` arithmetic, while the may-only-lower
direction stays green. Restored after.
efschu pushed a commit to efschu/htsglang that referenced this pull request Sep 9, 2026
…refill

ROOT of the on_idle wall that killed the serving base twice (boot weg2sn5n
2026-09-09T21:19:33Z, all three D ranks, `leaked_mamba_pages={26}`; boot ARM 3
before it, `={16}` after ONE 24k direct prefill).

NOT the sgl-project#935/sgl-project#936 census false positive, and that was checked first: `[full]`
balanced exactly (707585+50943==758528), TREE CENSUS was self-consistent on
every rank (`MAMBA: tracked_evictable=18 recomputed_evictable=18`), and
`_all_component_values_flatten` DFSs every node from the root regardless of
tier. World (b) of the invariant checker's own docstring: the row was orphaned
outside the tree.

THE DEFECT. This boot runs `--mamba-radix-cache-strategy extra_buffer` with
overlap, so a request holds THREE slots (active + two ping-pong). On the
finish path the unified component:

  * allocates a FRESH `new_slot` and calls `donate_mamba_ping_pong_slot`;
  * that method returns the OLD slot at `donate_idx` -- what the tree takes --
    and REPLACES the buffer entry in place, `buf[donate_idx] = new_slot`;
  * `cleanup_after_caching_req` then recomputed the SAME index as `keep_idx`
    and passed it to `free_mamba_cache`, which freed only `buf[1-keep_idx]`.

`buf[keep_idx]` is `new_slot`, owned by the REQUEST; the tree owns the old
slot. Keeping it handed it to nobody. Exactly one orphaned slot per finished
request whose donation the tree accepted -- which is why only NEW long
prefixes trip it (a request landing on an existing prefix takes
`mamba_exist=True` and never donated), and why a short-prompt fleet ran for
hours before the first Claude-Code-shaped 40-55k prompts killed it in minutes.

THE KEEP SEMANTICS WERE INHERITED AND INVALIDATED BY THE PORT, not lost as in
sgl-project#1051. The sibling this component replaced (`mamba_radix_cache.py:746-761`)
takes the tree's value from the buffer IN PLACE and allocates nothing:
    src_active  = req.mamba_ping_pong_track_buffer[keep_idx].unsqueeze(-1)
    mamba_value = src_active.clone()
There "keep buf[keep_idx]" is exactly right because the tree really owns that
entry. The unified port added the fresh allocation and the in-place
replacement while carrying keep_idx across unchanged.

SECOND LEAK, same branch, opposite direction: when the tree REFUSES the
donation the old slot has already left the buffer, so `free_mamba_cache`
cannot reach it and nothing else owed it back. The int8 branch directly above
handles its twin (`insert_value_unused`) and the plain branch below does too
(sgl-project#929); only this one handled neither. Both halves are fixed here, and the
rule now matches its two siblings instead of adding a second ledger.

INSTRUMENTATION (approved with this posten, no behaviour change):
* `#924D station=alloc_component` / `station=alloc_cow` close the coverage
  hole that made the specimen unrootable: `station=alloc` was emitted ONLY
  from scheduler.py, so every draw inside this component was invisible and
  slot 26 appeared in no station line at all.
* the leak report now names the OWNER, not just the slot: per leaked slot it
  prints `slot_used` (the allocator's own ledger), the #1033b provenance entry
  (last event + releaser stack) and whether an alloc group is open. Same
  complaint #1033b makes about `_refuse_double_free` -- the instrument named
  the visible party, not the responsible one.

Tests: test/registered/unit/weg2/test_mamba_pingpong_donation_ownership.py,
8 cases driving the REAL `donate_mamba_ping_pong_slot`, `free_mamba_cache` and
`cleanup_after_caching_req` against a REAL MambaSlotAllocator on CPU, and
asserting the invariant checker's own arithmetic (expected - free - tree)
after every station. RED-first against the unfixed tree: 5 failed / 3 passed
(the 3 are postcondition pins that hold both ways), reproducing the specimen
shape exactly -- one request, one orphan. 8/8 green on the fix. 3 mutants
killed with an unmutated control: pre-fix keep_idx restored (L1), refused-
donation give-back dropped (L2, and L1 still green under it, so the two halves
are independently covered), tree's slot freed as well (swings into sgl-project#1051's
double-owned direction and is caught by the allocator's double-free refusal).
ruff: invariant_checker.py's 2 findings are pre-existing and identical on the
unfixed tree; the other two files are clean.
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