Skip to content

Support muse glimmer - #4

Merged
jason-fxz merged 11 commits into
mainfrom
support-muse-glimmer
Aug 18, 2026
Merged

Support muse glimmer#4
jason-fxz merged 11 commits into
mainfrom
support-muse-glimmer

Conversation

@andy-yang-1

Copy link
Copy Markdown
Collaborator

No description provided.

… NVFP4)

Dense 30B text tower of the multimodal wrapper, served text-only like the
other VL checkpoints. Architecture-wise it is a mix of pieces we already
run -- gemma4's hybrid SWA/full layer split and qwen3.5's gated attention +
CT-NVFP4 dense path -- plus a few new wrinkles:

- [SWA x3, full] pattern where the full layers are NoPE (layer_rope_theta
  0): rope is skipped entirely on those layers, guarded so base=0 never
  reaches RotaryEmbedding
- weightless per-head qk RMSNorm with an extra query scale (3.87); the
  scale commutes with rope so it folds into AttentionSpec.sm_scale
- separate self_attn.gate_proj (sigmoid output gate), fused into the
  q/k/v projection since it reads the same normed layer input
- centered (1+w) sandwich norms with two eps (1e-5 pre / 1e-8 post),
  applied at runtime like M3's gemma norms, never baked into bf16
- NormedEmbedding (weightless RMSNorm after embed) and gemma-style logit
  softcap with a pre-scale (output_multiplier)

The RedHatAI NVFP4 release is llm-compressor W4A16 on every text Linear
(attn gate included), same layout as Qwen3.6-27B-NVFP4 -- the CT helpers
moved from qwen3_5_moe/weight.py into models/loader.py so both models
share them, and the W4A16 kernels are reused unchanged.

Server side the model speaks the ATEM channel protocol: to=self segments
are reasoning, to=user is content, tool channels carry
<atem:function_calls> invoke/parameter XML (string values not trimmed,
per the template's own contract). New reasoning parser + detector handle
the bare first segment (generation resumes after <|start|>assistant),
and reasoning_strength low/medium/high/xhigh rides the thinking gears,
with OpenAI reasoning_effort mapped onto it.
AutoConfig refuses to load a config.json whose model_type it does not know
when the checkpoint ships no auto_map (Muse-Glimmer needs transformers >=
5.15; MiniMax-M3 only loaded because nvidia bundled a remote config class).
FreeToken never instantiates modeling code -- parse_config just reads
fields -- so fall back to an attribute view over the raw JSON, wrapping
*_config sub-dicts the way PretrainedConfig nests them and leaving
everything else (rope_parameters etc.) as plain dicts.
The real RedHatAI checkpoint splits layer 49's down_proj across the shard
boundary: weight_packed sits in shard 1, its weight_scale/global in shard 2,
so reading siblings from the same file handle blew up mid-load. Route all
sibling lookups through a lazy shard-map reader keyed off the safetensors
index. Also pins the loaders with synthetic-checkpoint roundtrips: bf16
keys/fusion order against the model's state_dict, and the native-FP4 path
with cross-shard scales, reciprocal globals and dropped activation scales.
…mplate

The jinja chat template owns every special token (HF's apply_chat_template
tokenizes with add_special_tokens=False for the same reason), but the
templated path re-encoded with the defaults, so tokenizers that auto-add
bos (muse-glimmer, llama lineage) got a doubled bos at position 0. Found by
greedy-parity against a pure-torch Muse reference: 70 prompt tokens vs the
reference's 69, and a diverging generation. With the fix the served greedy
decode matches the reference token for token through the full turn. The
dsv4 encoder path keeps its existing behavior.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Review + e2e done: BF16 greedy matches transformers 5.15 char-for-char on H100; NVFP4, streaming, and all test suites pass; model math and weight loading are clean. Parsers were cross-checked against vLLM's muse_glimmer parsers (acb0f1dcd) on the same wires. All issues below are in the ATEM parsers.

HIGH

1. Channel close mid-invoke corrupts the next call (function_call_parser.py ~L3264). On a channel closer arriving mid-invoke, only _ps_reset() runs; current_tool_id and streamed_args_for_tool are left stale. The next channel's call reuses the same tool_index and the serving layer merges the two — worst case ("weather.get", {"path":"/tmp/x","content":"hi"}): valid JSON, wrong tool, no error. Triggered by a literal <|eot|>/<|eom|>/<|start|> inside a parameter value, or any truncated invoke followed by another call.
Fix: at the channel boundary do what GptOssDetector does — finalize prev_tool_call_arr[...]["arguments"], clear streamed_args_for_tool[...], current_tool_id += 1, then reset.

2. Headerless channel switch drops the call. The model sometimes leaves to=self without <|eom|>, writing to=<tool><|message|> directly (per vLLM: deterministic for empty-argument calls on tools with optional params). FT's only boundaries are <|eom|>/<|eot|>/<|end_of_text|>/<|start|>, so the whole tool channel is absorbed into reasoning and the call is lost, in every mode.
Fix: a complete to=<name><|message|> match is a segment boundary (vLLM's rule). This also allows deleting the _HEADER_HEADS sniffing in both parsers — commit to "header" only on a full match — which fixes MED-4 too.

MED

  1. Doubled tool names. The template renders a bare-name tool's recipient as name.*, so the model emits get_weather.get_weather; FT forwards it as an unknown tool. Fix: collapse head.tailhead iff head == tail and head is registered.
  2. Streaming emits invalid JSON arguments. A literal <|eom|> in a parameter value cuts the channel mid-value: streaming sends {"path":"/tmp/t","content":"stop token is (one-shot keeps the full value). Fix: on truncation, close the JSON or drop the call with a warning — never emit broken arguments.
  3. Bare ATEM block swallows all following text. After a bare block (no channel envelope) the detector stays in tool mode forever; trailing user text is lost in streaming, returned in non-streaming. Fix: return to text mode when the block closes.
  4. Content starting with assistant/to=/<|message|> is eaten as a header — swallowed up to the next <|message|> or buffered until finish. Fixed by HIGH-2's full-header rule.
  5. --reasoning-parser off, non-streaming: to=self<|message|>... leaks into content (markup included) and the to=user body is lost; streaming handles the same wire correctly. Fix: run the same channel classification in detect_and_parse.

LOW

  • Preserved tool-channel slice has no partial-marker hold-back (reasoning_parser.py ~L717): content can shrink after emission, leaking <|start| debris.
  • A headerless reply that starts with header-lookalike bytes streams as nothing and is dropped at flush (vLLM has the same limitation).
  • RawConfigShim blocks all _-names, hiding _name_or_path — DSV4 on old transformers dies with a misleading error. Fix: special-case _name_or_path.
  • One-shot _param_re lookaheads truncate values containing literal </atem: tags.
  • Truncated tool channel silently returns "no call" — indistinguishable from the model not calling. Log a warning (vLLM does).

Echo-becomes-execution risk

FT executes <atem:invoke> found in bare blocks or to=user bodies. The system block itself contains a literal ATEM example; if the model echoes it, the quote becomes a real tool call. vLLM only executes markup inside a tool-recipient channel. Fix: adopt that rule, or document why not.

…ncation, execution scoping

Review findings, cross-checked against vLLM's muse_glimmer parsers:

- A channel closer arriving mid-invoke now finalizes the open call: the
  streamed argument JSON is closed (never emit broken arguments), completed
  parameters land in the ledger, and the ordinal advances so the next
  channel's call can't merge into it. Truncated channels log a warning
  instead of vanishing silently.
- A complete headerless "to=<name><|message|>" is a segment boundary in both
  parsers (vLLM's rule) -- the model leaves to=self without <|eom|> for
  empty-argument calls, which used to absorb the whole tool channel into
  reasoning. Committing to the bare stream-start header now also requires a
  FULL match (with a length bound), so content that merely starts with
  "assistant"/"to=" streams as text instead of being eaten; the verbatim
  <|start|>assistant normalization became unnecessary and is gone.
- ATEM markup is executed only inside a tool-recipient channel: a block
  quoted in a to=user body -- or the system prompt's own ATEM example echoed
  back -- renders as text instead of becoming a real call (vLLM's scoping).
- Template-doubled recipients (a bare-name tool renders as name.* and the
  model emits get_weather.get_weather) collapse to the registered head, via
  a new name-normalization hook on InvokeParamStreamMixin.
- detect_and_parse is now a replay of the streaming machinery, so both paths
  share one definition of channel classification (a raw to=self body never
  leaks into content with --reasoning-parser off), execution scoping, and
  value semantics (the one-shot lookahead regexes that truncated values
  containing literal </atem: tags are gone with them).
- The preserved tool-channel slice holds back partial markers while
  streaming, so emitted content can no longer grow a "<|start|" that the
  next chunk reveals to be the next segment's opener.
- RawConfigShim serves _name_or_path through the underscore guard (DSV4's
  parse_config reads it to find inference/config.json).

Re-validated on the real NVFP4 checkpoint: greedy parity prompt unchanged,
bare-name and namespaced tool calls, streaming argument JSON valid.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Round 2, on 1f78be0 (post review-batch). The batch resolved everything from the first pass; the items below are new, or edges opened by the new code paths. All behavioral claims verified by probes executed against this head.

HIGH

1. Exiting a tool channel without a terminator drops the rest of the turn (streaming) (reasoning_parser.py ~L791). An edge of the batch's new headerless-switch support: when the tool channel ends via an abutting <|start|> header or a headerless to=user<|message|>, emit_tool appends the boundary token only for kind=='closer', so the preserved block reaches the detector with no trailing boundary; the detector never leaves tool mode and discards all remaining user-visible text (function_call_parser.py ~L3300). Wire to=search<|message|><atem:function_calls>…</atem:function_calls><|start|>assistant to=user<|message|>visible answer<|eot|> through the default SSE pipeline: call emitted, content '' — with <|eom|> it survives, and non-streaming survives only via parse_non_stream's tail recovery. vLLM classifies this wire correctly.

2. max_tokens mid-invoke still ships invalid JSON arguments (function_call_parser.py ~L3331). The batch finalizes at channel boundaries, but the end-of-stream path is not wired: finish_streaming resets state without _finalize_truncated_invoke, and both serving fallbacks miss — unstreamed_arguments returns None because prev_tool_call_arr[i]['arguments'] is the falsy {} (the if args: guard, ~L3558), and recover_truncated_call finds no markup since the detector already consumed it. Truncating at <atem:parameter name="city">Par streams ("weather.get", '{"city":"Par') — an unterminated string — while detect_and_parse on the identical text closes it. Fix: run the finalizer at stream end too, honoring its own "never emit broken arguments" contract. (vLLM's muse parser drops truncated calls with a warning, tests asserting it.)

3. Non-streaming drops the second invoke block in one channel (function_call_parser.py ~L3315). The mixin defers post-call text (idle-mode if calls: break), and the channel-close transition then discards the un-reprocessed second block as markup debris. Two invokes + closer in the same undrained buffer — always true for the one-shot path now that detect_and_parse replays the streaming machinery — yields only the first call (the second vanishes, not even as text), while char-by-char streaming yields both. The second tool call silently never executes on non-streaming requests. vLLM captures all blocks (test_parallel_calls_across_eom_boundaries).

4. reasoning_strength in the module-global _THINKING_KWARG_KEYS kills the thinking toggle for every other family (model_meta.py ~L77). effort_toggle_kwargs('qwen3', 'high', {'reasoning_strength':'high'}) returns {'enable_thinking': True, …} on main but only the passthrough on this branch (same for glm / minimax_m3's thinking_mode) — a gateway forwarding muse-style kwargs to another family's server silently loses the toggle. Recognize the key only when reasoning_parser == 'muse_glimmer'.

MED

5. MuseGlimmerReasoningParser streaming is O(n²) (reasoning_parser.py ~L678). self._buffer is never trimmed and _scan re-walks it from byte 0 on every chunk, rebuilding the full reasoning/content strings. Measured on this branch: 2000/4000/8000 five-char increments cost 0.03/0.14/0.93 s — clean 4× per doubling. This family reasons by default; a 100k-char CoT stream burns minutes of pure-Python CPU on the async serving loop, stalling SSE for all concurrent streams. This PR's own MuseGlimmerDetector channel layer already does it right (trim consumed prefix + bounded hold-back) — same treatment here. Caveat for cross-checking: vLLM's muse parsers won't surface this — they re-scan (and re-decode the full token sequence) per delta too; the right references are vLLM's token-ID think-tag base and the harmony StreamableParser. GptOssHarmonyReasoningParser (main) has the identical defect and is worth fixing in the same pass.

6. End-of-stream flush drops held text instead of delivering it. Two confirmed shapes, one fix: (a) a complete response that merely looks like a bare header prefix (assistant, to=me@example.com — ≤96 chars, <|message|> never arrives) stays "undecided" and is dropped at flush by both finish_streaming (~L3334) and the reasoning flush (~L777), returning an empty response — and since detect_and_parse now replays streaming, non-streaming loses it too (vLLM's one-shot returns it verbatim); (b) a literal stray <|eot|> in prose ("The token <|eot|> ends a turn.") streams as "The token " with the tail lost at flush, while one-shot returns the full sentence (reasoning_parser.py ~L785). Fix: at EOS, flush undecided/held text as content rather than dropping it — harmony flush()'s rule ("deliver, don't drop"). Note (b) is broken the same way in vLLM's muse parser, so it can't serve as the check here either.

Small

  • detect_compressed_tensors_nvfp4 (models/config.py ~L54) matches num_bits==4 + type=='float' without group_size==16, so a compressed-tensors MXFP4 checkpoint (group_size=32; real since LLM Compressor 0.9.0) routes into the NVFP4 loader and dies in a confusing KeyError/shape assert. vLLM gates on strategy==tensor_group + group_size==16 (_is_nvfp4_format); the same two conditions here turn it into a clear "unsupported scheme" error.
  • The cross-shard scale resolution _ShardReader embodies (the layer-49 case this PR diagnosed) should reach qwen3_5_moe's CT path too — it still resolves NVFP4 scales through the shard-local handle (qwen3_5_moe/weight.py ~L598) and will crash on the same shard layout. While at it, the reader could live in models/loader.py next to nvfp4_parts_ct instead of becoming the sixth per-family copy.
  • Minor dedup: _try_fuse reimplements loader.ct_bf16_fuse with a different return protocol, _CT_SCALE_SUFFIXES is copied verbatim from qwen3_5_moe, and quant.py's factories duplicate qwen3_5_moe/quant_linear.py's (which also handle fp8). Also: a warning log when tool-channel prose is discarded (function_call_parser.py ~L3300) would save the next person a debugging session.
… truncation, O(n) streaming

Second review pass on the ATEM parsers, plus the shared-infra cleanups it
flagged:

- A tool channel that ends without its own terminator (abutting <|start|>
  header or headerless to=user switch) now reaches the detector as a
  delimited block: the reasoning parser appends a synthetic <|eom|> to the
  preserved slice, so the rest of the turn streams instead of being
  discarded in tool mode.
- max_tokens mid-invoke: a finalize_stream hook runs at end-of-stream (the
  same closing-fragment logic the channel boundary uses), so the client's
  concatenated argument fragments end as valid JSON -- verified on the wire
  at several truncation points, including mid-string. unstreamed_arguments
  also stops treating the {} ledger entry as absent.
- The tool-mode boundary transition loops the mixin until it is inert
  before consuming the closer: a second invoke block in the same undrained
  buffer (always the case for the one-shot replay) parses instead of
  vanishing as markup debris.
- reasoning_strength counts as a thinking kwarg only for muse_glimmer;
  globally it swallowed every other family's toggle mapping when a gateway
  forwarded muse-style kwargs.
- The reasoning parser streams by consuming its buffer (the full re-scan
  per chunk was quadratic and stalled the serving loop on this family's
  long default CoT), and end-of-stream flush now delivers held text --
  a reply that merely looks like a bare-header prefix, or prose stranded
  after a literal closer -- instead of dropping it. Discarded tool-channel
  prose leaves a warning.
- detect_compressed_tensors_nvfp4 gates on group_size 16 + tensor_group
  (vLLM's rule); an MXFP4 checkpoint fails with a clear unsupported-scheme
  error instead of a shape assert inside the NVFP4 loader.
- Dedup: ShardReader and the CT scale suffixes moved to models/loader.py
  and qwen3_5_moe's CT path now resolves scales through the shard index
  too (same layer-49 splitting hazard); the quant-linear factories moved
  to models/quant_linear.py, replacing muse's copy.
Main's #5 replaced the per-family thinking-gear registry with checkpoint
probing (tokenizer/effort.py), which supersedes the muse entries this branch
had added to model_meta -- resolved by taking main's side and mapping muse
onto the new machinery where it belongs: the render layer now broadcasts
reasoning_effort as reasoning_strength too (the same every-spelling rule the
thinking toggles use; Jinja ignores undeclared variables and an explicit
caller spelling wins). Through the broadcast the probe sees muse's template
grade effort (validating nothing, defaulting high), so /v1/cache/status
derives the OpenAI gear triple with default high, and muse's native xhigh
still passes quantization untouched. The double-bos fix is re-applied to the
reworked tokenize path: template-rendered prompts encode with
add_special_tokens=False, raw strings and the dsv4 encoder keep the default.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Round 3, on 5076b6c (round-2 batch + the main merge). Everything from round 2 verifies as fixed at this head: item 1 across both switch shapes including full turns; item 2 across 8 truncation points × 5 chunkings with client-side JSON reassembly, streaming ≡ one-shot; item 3 across the block/invoke matrix (and the new loop-until-inert terminates — the mixin can't eat the closer); item 4 preserved architecturally by the merge (explicit reasoning_strength still wins, the probe detects muse's grading, gears low/medium/high default high, native xhigh untouched); items 5/6 for the body/start paths and both EOS shapes. The loader dedup is behavior-preserving under real-fixture A/B — the layer-49 cross-shard split is real in the RedHatAI index, the old code did raise on it, and the new reader's index-less fallback works. The merge resolution was diffed against the auto-merge: nothing lost from either parent, 43/43 muse tests retained, single-bos verified with the real tokenizer. The items below are the residue plus edges opened by the new paths; all behavioral claims were probe-verified against this head.

MED

1. Item-5 residual: "seek" mode is still O(n²), holds an unbounded buffer, and stops streaming entirely (reasoning_parser.py ~L759). The rewrite makes the body/start paths consume the buffer, but between segments the seek branch re-runs buf.find(ATEM_START) and ATEM_INLINE_HEADER_RE.search(buf) from byte 0 every chunk and releases nothing until flush. Five-char increments after …<|eom|>: 0.008 / 0.029 / 0.127 / 0.555 s for 2k/4k/8k/16k increments — the same 4.4×-per-doubling shape item 5 was filed for, just a ~3× smaller constant. A 160-kchar headerless continuation burns 2.3 s of frontend CPU, and the client sees 0 bytes for 20k chars, then one blob at EOS. This is exactly the off-protocol shape the branch exists to serve (a repetition loop after <|eom|> produces it). Fix: remember how far the buffer is already proven marker-free and scan from there (buf.find(ATEM_START, off) with a ~96-char overlap) to restore O(n); to fix the stall, release seek text eagerly with the same ~96-char holdback body mode uses — L783 already ruled that held seek text is content.

2. Seek-mode EOS still drops text around a complete <|start|> whose <|message|> never arrives (reasoning_parser.py ~L766). The batch's deliver-don't-drop rule covers two of the three hold paths. In seek mode, if s > 0: self._buffer = buf[s:] pre-drops prose before the marker, and if final: self._buffer = "" drops everything after it — unbounded. to=user<|message|>The token <|start|> opens a segment. More text. + EOS yields The token — while the same sentence with a literal <|eom|> now survives in full, an asymmetry new to this batch. …answer<|eom|>more text <|start|>assistant + EOS drops more text . Fix: at final, emit buf[:s] as content (defer the debris drop until the header actually completes), and cap what is discarded from s onward at a plausible header length instead of clearing the rest of the turn.

3. The merge dropped the branch's effort folding: out-of-vocabulary efforts reach the model verbatim (tokenize.py ~L104 + effort.py quantize_effort). Muse's template grades reasoning_strength without validating it, so the probe returns validates=False with supported = all 7 scale names; quantize_effort then passes everything through (value in profile.supported) and the broadcast interpolates it: reasoning_effort="minimal" renders Reasoning strength: minimal. — same for max, and none via chat_template_kwargs — while the model card documents exactly low/medium/high/xhigh as the trained levels. Both parents prevented this: the branch folded minimal→low (that assertion was deleted in the merge) and main's design quantizes onto the checkpoint's vocabulary. /v1/models also advertises minimal/none in supported_reasoning_efforts for muse. Fix: for validates=False profiles, don't treat the whole scale as pass-through vocabulary — quantize the never-advertised names onto the nearest offered gear (minimal→low is within _MAX_QUANTIZE_DISTANCE), keeping low/medium/high/xhigh untouched so native xhigh still passes.

4. A literal channel-marker inside a parameter value splits the call — and the remainder now surfaces as raw markup in user content (reasoning_parser.py ~L699). _segment_body_end matches <|eom|>/<|eot|>/<|start|>/to=X<|message|> anywhere in a tool-channel body, including inside an open <atem:parameter> value: fs.write with content send to=user<|message|> please executes with {"content": "send "} and please</atem:parameter>\n</atem:invoke>\n</atem:function_calls> is delivered to the user as visible content. The truncation itself is the known round-1 string-level ambiguity (and it warns), but before this batch the tail was dropped; the raw-markup leak arrives via the item-6 EOS delivery / inline-switch reclassification. Hardening: when a channel boundary fires mid-invoke and _finalize_truncated_invoke runs, route that channel's non-markup residue through _drop_channel_prose instead of letting it re-enter as content; optionally prefer boundaries outside an open parameter span.

Small

  • A redundant non-stop closer in seek mode leaks verbatim: …42.<|eot|><|eot|> → content …42.<|eot|> on the no-tools path (_leaked_special_tokens is empty for muse; with tools the detector strips it — function_call_parser.py ~L3356 does exactly the filtering the flush omits). Checked against the checkpoint: the generation eos ids are <|end_of_text|> and <|eot|> (suppressed by the detokenizer when they stop generation), so the realistic trigger is a doubled/redundant <|eom|> from a degenerate decode. Same one-line fix as the detector: strip ATEM_CLOSING_TOKENS from the seek-EOS delivery.
  • /v1/cache/status no longer offers xhigh: derive_think_gears offers only the OpenAI triple for a non-validating template, but the model card explicitly recommends xhigh for coding/agentic use, and the branch advertised it. Consider extending the gears with probed above-high names for such profiles (manual reasoning_effort="xhigh" still works).
  • Stream vs one-shot differ on a literal stray closer with no tools: detect_and_parse's passthrough shortcut checks only <|message|>/<|start|>, so The token <|eot|> ends a turn. keeps the closer in the one-shot response but streams without it. Route closer-containing text through the streaming replay too.
  • _drop_channel_prose fires per streamed fragment — token-by-token serving emits one WARNING per generated character of channel prose. Accumulate and warn once at the channel boundary (where _finalize_truncated_invoke already logs).
  • detect_compressed_tensors_nvfp4 validates only the first 4-bit-float group: the pre-existing return True short-circuits the loop, so {group_0: nvfp4(16/tensor_group), group_1: mxfp4(32/group)} is accepted (and routed into the NVFP4 loader) while the reverse key order raises the new clean error. Collect verdicts across all groups before returning.
…t vocabulary, channel residue

Third review pass, all on the ATEM parser edges and the effort merge residue:

- The reasoning parser's seek mode (between segments) now streams eagerly as
  content with the standard hold-back and consumes its buffer -- the
  hold-until-flush behavior was still quadratic and sat on a headerless
  continuation (a post-<|eom|> repetition loop) for the whole stream. Seek
  deliveries strip redundant closer tokens (the detector's rule), the
  one-shot passthrough shortcut routes closer-containing text through the
  replay so both paths agree, and a complete <|start|> whose <|message|>
  never arrives is bounded: text before it is content immediately, the
  candidate is held only to a plausible header span (released as literal
  text past it, capped debris at EOS) instead of eating the rest of the turn.
- Out-of-vocabulary efforts no longer reach the model verbatim: a
  non-validating grader is served its dialect's ladder instead of the whole
  scale -- the probe now also learns WHICH spelling the template reads, so
  the reasoning_strength dialect (muse; its card documents xhigh) gets
  low/medium/high/xhigh while plain effort graders keep the OpenAI triple
  (the gpt-oss-shape pin stays intact). minimal quantizes to low, max to
  xhigh, none falls to the template default; /v1/models and the
  /v1/cache/status gears both advertise the served vocabulary, xhigh
  restored to the gear list.
- A channel boundary that fires mid-invoke marks the channel truncated:
  until the next boundary the detector routes incoming text (the broken
  channel's raw ATEM residue) through the dropped-prose path instead of user
  content -- a literal to=user<|message|> inside a parameter value no longer
  leaks "</atem:parameter>..." into the visible reply. Dropped channel prose
  is accumulated and logged once per channel rather than per fragment, and
  the detector mirrors the header-span bound in text mode.
- detect_compressed_tensors_nvfp4 collects verdicts across ALL config groups
  before returning: a mixed {nvfp4, mxfp4} checkpoint now raises the clean
  unsupported-scheme error in either key order.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Round 4, on 031b506b7. Round-3 status: items 1/3 and all five smalls verify as fixed (seek streams eagerly at O(n) — 2k/4k/8k/16k increments now 2.1/3.9/7.7/15.5 ms, peak buffer 35 chars on a 40k continuation; effort dialect works end-to-end on the real checkpoint — minimal→low, max→xhigh, none→default, xhigh back in the gears and /v1/models advertises only the served vocabulary; closer stripping, warn-once, and the config-groups fix all pin under mutation). Item 2 is fixed as the new test pins it: text before the marker survives and the drop past a stray <|start|> is bounded at ≤137 chars instead of unbounded — a sub-span tail at EOS still dies as capped debris, which the code comment classifies as deliberate; fine by me. Item 4's markup leak is fixed, but the fix itself regressed — the HIGH below. A 402-wire old-vs-new corpus shows no other unintended divergence. Everything below was re-verified by hand against this head and against 5076b6c for the regression claims; correctness issues only.

HIGH

1. _truncated_channel never clears on the serving pipeline: after a truncated invoke, the entire rest of the turn's user-visible reply is silently dropped (function_call_parser.py ~L3188 set; ~L3374-3383 boundary consumed without clearing). The mark routes text to _drop_channel_prose until a boundary reaches text mode — but in production order (reasoning parser first, its content feeds the detector) that boundary never arrives: user-segment headers are unwrapped upstream, user-body closers are consumed, emit_seek strips stray closers, and the tool slice's own closer is consumed in tool mode at L3374 with the mark left set. The clear sites (L3305/3311/3331) are all in text mode, and finish_streaming returns '' outright while the mark is set (L3391-3394). Two verified wires, every chunking (1/3/64):

  • Round 3's own MED-4 wire + <|eot|><|start|>assistant to=user<|message|>Done.<|eot|> → head: content '' (the warning shows the swallowed reply: dropped 64 chars …</atem:function_calls>Done.); parent 5076b6c: Done. delivered (with the residue leak this fix removed).
  • Simplest trigger, no literal markers involved — an invoke left unclosed before an abutting user header: to=weather.get<|message|><atem:function_calls>…<atem:parameter name="city">Par<|start|>assistant to=user<|message|>The weather tool is on it.<|eot|> → head: content ''; parent: The weather tool is on it. delivered cleanly (no leak at all — on this shape the parent had no bug and this commit loses the whole reply).

The mark is also set when the boundary that fired was itself a closer (the channel ended; there is no residue to guard against). Fix: don't gate on "next boundary" — clear the mark immediately when the firing boundary was a closer, and for the abutting-start/inline-switch case drop by shape: consume only runs of ATEM closing markup (</atem:parameter>/</atem:invoke>/</atem:function_calls> plus whitespace) and clear at the first non-markup character. Please also add a production-order test via the file's own _pipe helper — the new detector-only pin passes while the pipeline loses the reply, and this is the second time this area's standalone-detector tests have been green over a pipeline regression.

MED

2. The header-span cap is bypassed whenever a <|message|> is already in the buffer (reasoning_parser.py msg≠-1 branch; detector mirror function_call_parser.py ~L3315). The bound runs only on the msg == -1 branch, so a stray literal <|start|> followed — arbitrarily far — by the next segment's <|message|> is parsed as one giant header. Verified: to=user<|message|>ok<|eom|><|start|> + 300×P + real header + more → streaming delivers all 315 chars, detect_and_parse returns okmore (309 chars silently swallowed). Worse, ATEM_RECIPIENT_RE picks the first to= inside the junk: with to=self in the swallowed span, one-shot routes the following user answer into reasoning (verified: content first., reasoning the actual answer) while streaming delivers it as content. The swallow itself is pre-existing (parent returns okmore in both modes), but the stream/one-shot divergence is new — the cap was applied to one of the two branches. Fix: apply the span bound before honoring a found <|message|> in both layers: if msg - s - len(ATEM_START) > ATEM_HEADER_SPAN, release the marker as literal content and resume scanning, exactly like the msg == -1 branch.

3. Span release breaks protocol-legal long headers in streaming — trailing reply lost from span ~119, the tool call itself lost from span ~129 (reasoning_parser.py ~L800). ATEM_RECIPIENT_RE accepts unbounded recipient names, so a header longer than the span is protocol-legal but gets released mid-arrival. Verified gradient on head, wire = long-name tool channel + <|eot|> + to=user reply, vs parent (correct at every length and step size):

name len (span) step=1 step=5 one-shot
102 (115) ok ok ok
106 (119) call ok, reply lost ok ok
110 (123) call ok, reply lost call ok, reply lost ok
116 (129) call lost, raw header shown to user same ok

The release fires while <|message|> is still mid-arrival (hence loss starting ~9 chars before the nominal 128), the inline-switch recovery can't re-match (its regex caps names at 64), and streaming/one-shot disagree on which recipients are valid. Long MCP-style tool names make this reachable. Fix: cap ATEM_RECIPIENT_RE at 64 chars to match ATEM_INLINE_HEADER_RE/ATEM_BARE_START_RE (both modes then agree, and >64 degrades identically), and only release once len(buffer) - len(ATEM_START) > ATEM_HEADER_SPAN + len(ATEM_MESSAGE) so a header whose <|message|> is mid-arrival isn't cut.

4. Deliver-then-swallow across layers at EOS (function_call_parser.py ~L3396). The reasoning parser measures its span on raw bytes, but emit_seek strips closer tokens from what it delivers, so the detector receives fewer bytes; when the stripped tail sits at or under len(ATEM_START)+span at EOS, finish_streaming's blanket ATEM_START in residual → '' swallows content the layer above explicitly decided to deliver. Verified: to=user<|message|>answer<|eot|>prose <|start|> + 50 chars + 15×<|eom|> — parser-only delivers 71 chars, the full pipeline returns answerprose at every chunking (the released <|start|>+50 chars gone). Bounded (≤ ~137 chars) but silent, tools-present only. Fix: make finish_streaming mirror the capped-debris rule — deliver residual text and discard at most the <|start|>+span candidate — instead of dropping any residual containing ATEM_START.

…shape, span bounds unified

The round-3 residue guard regressed on the production pipeline: the mark
gating text into the dropped-prose path waited for a boundary that the
pipeline never delivers (the reasoning parser terminates every preserved
slice and unwraps everything else), so a single truncated invoke silently
swallowed the rest of the turn's reply. The mark now clears by SHAPE: text
mode consumes only runs of ATEM closing markup (closing tags + whitespace)
and releases at the first non-markup character -- trailing tag debris still
drops, a real reply flows immediately, and the mark deliberately survives
the synthetic terminator because the pipeline delivers the residue after
it. Production-order (_pipe) tests now cover both reviewer wires, plus a
wire corpus (historical degenerate shapes x chunkings) asserting
streaming/one-shot/pipeline agreement -- standalone-detector tests being
green over a pipeline regression is the failure mode this file has now hit
twice.

The header-span bounds are unified across the remaining shapes: the bound
applies whether or not a <|message|> is already in the buffer (a stray
literal <|start|> + junk + the next segment's real header no longer parses
as one giant header, which also let a to= inside the junk hijack the
recipient and diverge one-shot from streaming); recipient names cap at 64
everywhere (matching the inline/bare-start rules, so both modes agree and
longer names degrade identically); the release condition gains
len(<|message|>) slack so a protocol-legal long-name header whose marker is
mid-arrival isn't cut; and the detector's finish_streaming mirrors the
capped-debris rule instead of blanket-dropping any residual containing
<|start|> -- text the layer above deliberately delivered stays delivered.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Round 5, on e5d6cfec5. Three of the four round-4 items verify as fixed, cross-checked against both 031b506b7 and 5076b6c:

  • Item 1 (truncation mark): fixed. Both reviewer wires deliver their replies at every chunking; the in-value-marker shape now leaks the broken channel's residue exactly as 5076b6c did (minus a leading space) — I read the "(as re-scoped by R4 HIGH-1)" tests as choosing leak-over-loss deliberately, and that's the right call: at string level the value tail is indistinguishable from a reply, and grandparent parity is the correct floor. The pure-closing-tag residue case is now strictly better than 5076b6c (tags dropped, reply kept, one warning).
  • Item 2 (span bound in the msg-found branch): fixed. Streaming and one-shot are byte-identical on a 307-wire corpus across rp/detector/pipeline (zero divergences, zero crashes — the stated goal of the batch, achieved); the recipient-hijack wire routes to content in both modes; the threshold sits at exactly 128/129 in both layers, and the 129–139 slack asymmetry between the two branches is unreachable for chunking-dependent divergence (verified by sweep plus the convergence argument: a ≤128 header never exceeds the 139 hold bound, and anything released early is rejected by the msg-found branch anyway).
  • Item 3 (recipient names): fixed. Names 60–115 now work perfectly everywhere (the 64-char regex cap only truncates the classification string; the invoke name names the call — verified including a registered tool whose name is another tool's 64-char prefix), and ≥116 degrades identically in both modes. Noted, accepted: one-shot at ≥116 no longer executes the call (it did at both parents) — the flip side of making the modes agree for names outside the 64-char contract.

Item 4 is not fixed, and the EOS-discard family it belongs to got worse. The three items below share one root cause and one fix.

HIGH

1. A real to=user reply after a stray <|start|>+junk segment is silently dropped — worse than both parents (function_call_parser.py ~L3437 with reasoning_parser.py ~L787). The new mechanisms interact: the msg-too-far branch releases the stray marker as content, so the detector receives the concatenated <|start|>{junk}REPLY; with no <|message|> in it, the detector holds it as a header candidate under the widened len(start)+span+len(message) = 148 release threshold; finish_streaming's capped-debris rule then discards the entire residual. Wire …<|eom|><|start|> + N junk chars + <|start|>assistant to=user<|message|>Here you go.<|eot|>: at head the reply is lost for N in [103..127] at step 1, step 7 and one-shot; 031b506b7 lost only [100..116] (and delivered at one-shot); 5076b6c never lost it. The +len(<|message|>) slack is what pushes these lengths under the hold threshold, and the capped-debris rule is what eats them.

MED

2. Round-4 item 4 persists: the two layers still measure the header span on different byte streams, and the new finish_streaming branch is a behavioral no-op (function_call_parser.py ~L3444). The round-4 reviewer wire is byte-identical to 031b506b7 (parser delivers 71 chars, pipeline 12, all chunkings). The deliver-verbatim arm of the new rule is unreachable: the reasoning parser's span runs on raw bytes but emit_seek strips closer tokens from what it hands down, so the detector's copy of any tail the parser released is shorter and always sits under the detector's own identical cap — forcing the arm to always-drop changes nothing in 12,000 fuzz wires and breaks no test. The commit's own comment ("its span runs on raw bytes; ours runs after closer stripping") names the mismatch the rule doesn't close. Realistic PoC, ordinary terminators only: to=user<|message|>The wire format is: <|start|> + 119 chars of prose + <|eot|><|end_of_text|> → parser delivers the full sentence, pipeline delivers The wire format is: — 127 chars swallowed, 119 of them plain prose; the same wire without the trailing closers agrees perfectly, so the closer-stripping is precisely what desynchronizes the spans. Parser-vs-pipeline divergences on a 4,000-wire fuzz went 381 → 407 at this commit.

LOW

3. The +len(<|message|>) slack widened the symmetric drop window: a literal <|start|> followed by 129–139 chars of real reply at EOS was delivered at 031b506b7 and is now discarded by both layers (reasoning_parser.py ~L814/818).

One fix closes 1–3: at end of stream, a <|start|> candidate that never received its <|message|> is not a header — emit the tail as content and drop only the marker, in both layers (reasoning_parser.py L818-819 and finish_streaming). That removes the discard window entirely instead of moving its edge, makes the two layers' agreement trivial (both deliver), and lets the unreachable deliver-verbatim branch go away.

4. A reply that literally begins with a complete ATEM closing tag is eaten after a truncated invoke (_channel_residue_end, ~L3241): </atem:parameter> is the closing tagis the closing tag; 5076b6c delivered it verbatim (the only spot in item-1 territory where head is worse than grandparent). Partial-tag-shaped text (</a>, </atem as prose) is held and released intact, so no ordinary word can be eaten — acceptable as the cost of shape-based dropping if you want, but worth a comment in the code.

Test integrity

The batch added production-order tests (good — the corpus does catch round-4's HIGH on 031b506b7), but three of the four pins don't hold their weight, which is how this family keeps regressing:

  • The R3 residue pin was edited (…never_reaches_content…markup_residue_dropped_reply_kept) by removing the prose from the wire; with the original wire restored, head fails the retained '</atem:' not in normal assertion (it now leaks the closing tags where 031b delivered clean Done.). If leak-over-loss is the intended trade — I agree it is — pin the trade explicitly on the original wire instead of weakening the wire.
  • test_eos_capped_debris_agrees_across_layers (the only item-4 pin) passes with finish_streaming reverted to the old blanket drop; its final A or B assertion is satisfied by A on every run, and its docstring claims the opposite of what the test observes.
  • Item 2's detector-half span bound has zero pins: deleting the block leaves the full suite green, yet on the pipeline it silently costs a tool call plus ~200 chars at one-shot granularity.
  • Corpus nits: "<atem:" not in normal cannot see </atem: leaks; corpus wire 4 is labeled "stray literal closer inside the reply" but contains none (duplicate of wire 5); the one-shot cross-check compares call names only.
Round 5 landed on the root cause of the whole EOS-discard family: the two
layers measure the header span on different byte streams (raw vs
closer-stripped), so any capped-discard window at end of stream has edges
that can never agree -- fixing one wire just moved the edge onto another.

Remove the window instead of moving it: at end of stream a <|start|>
candidate that never received its <|message|> is not a header. Both layers
now deliver the tail and drop only the marker. This closes the R5 HIGH
(reply after a stray start+junk segment dropped), makes the parser/pipeline
span agreement trivial (R4 item 4), and removes the symmetric 129-139 drop
window, all with one rule.

Also per review: document the known cost of shape-based residue dropping
(a reply that literally begins with a complete closing tag loses that tag),
and repair the test pins that weren't holding weight -- the R3 residue
trade is pinned on the original wire with exact expected output, the EOS
agreement test now fails against the old blanket drop, the detector's
giant-header bound gets its own pin, and the corpus checks closing-tag
leaks and argument payloads, not just call names.
@jason-fxz

Copy link
Copy Markdown
Collaborator

Round 6, on f4085fc. All three round-5 items verify as fixed — parser-vs-pipeline prose divergences on the 4,000-wire fuzz went 951 → 0, streaming ≡ one-shot everywhere, and the repaired pins genuinely fail against the old code under mutation. What's left to change:

1. (MED) A bare-header lookalike ending in <|message|> still returns an empty turn (function_call_parser.py ~L3442). to= + 70 chars + <|message|> (recipient past the 64 cap), assistantto=x<|message|>, or >8 leading newlines + to=user<|message|> (the header regex allows {0,8} of whitespace while the undecided-hold lstrips unbounded): the reasoning parser delivers the text as content, the detector's if ATEM_MESSAGE in residual: return "" blanks it at EOS. Pre-existing; last member of the family this commit closed.

Fix it by deleting the guessing layer instead of patching it: initialize both parsers from the prompt they continue. The chat generation prompt always ends with <|start|>assistant, so chat requests start the reasoning parser and the detector in header-open state; raw /v1/completions and assistant-prefill requests pass their own prompt-tail parse state (the frontend already renders every prompt for prerender_error, so the tail is at hand). Turn-start bytes then go through the ordinary full-header machinery this PR has already hardened: a real <|message|> token closes the header (a junk recipient yields an empty channel, which is what the model said), and a header that never gets its <|message|> falls to the existing "unfinished header at EOS is text" rule. Then delete ATEM_BARE_START_RE, atem_bare_start_state, the parser's "start" mode, the detector's _at_stream_start commit logic, and this blanket — the ~70 lines of guessing duplicated across the two layers are where this bug class keeps spawning. This is the same mechanism force_reasoning already is for the <think> families, read from the prompt instead of re-derived.

2. Corpus test: add the shapes it exists to police. It passes 21/21 against e5d6cfec5 — the commit with both round-5 bugs — because no wire contains a stray <|start|>+junk or ends inside an unfinished header. Add: " to=user<|message|>ok<|eom|><|start|>" + "J"*115 + "<|start|>assistant to=user<|message|>ok<|eot|>", a wire ending "<|start|>" + "p"*119 + "<|end_of_text|>", and one truncated invoke leaving real closing tags so the </atem: clause can fire. Also split assert _CORPUS_REPLY in one.normal_text or _CORPUS_REPLY in rp.normal_text — the right operand is always true, so the one-shot content is currently unchecked (blanking detect_and_parse's content entirely stays green).

3. PIN3 docstring (test_detector_giant_header_bound_pinned): with the detector bound deleted, the tool call still executes correctly — what actually breaks is ~300 chars of user content vanishing, caught by the junk/content assertion, not the calls assertion. Reword so a future reader doesn't relax the load-bearing half.

Mergeable from my side once item 1 lands.

… the guessing layer

The last member of the empty-turn family (round 6): a bare-header lookalike
ending in <|message|> -- a recipient past the 64-char cap, a glued
assistantto=x, or >8 leading newlines -- was delivered by the reasoning
parser but blanked by the detector's end-of-stream debris rule. Every such
bug came from the same place: both layers GUESSED whether the turn's first
bytes were a bare header, each with its own regex and its own hold logic.

Stop guessing. Every request that reaches these parsers is a templated chat
generation whose prompt ends with <|start|>assistant, so the reasoning
parser now starts header-open: it seeds a synthetic <|start|> and the
turn's first bytes go through the same full-header machinery as every later
segment (the same mechanism force_reasoning is for the <think> families,
read from the prompt instead of re-derived). The detector takes the state
as a constructor flag, on when it receives raw turn bytes without the
reasoning parser stacked above. A junk recipient now yields the empty
channel the model asked for; a header that never gets its <|message|>
falls to the unfinished-header-at-EOS-is-text rule; the synthetic marker
itself is never delivered. The bare-start regex, the undecided-hold
classifier, the parser's "start" mode, the detector's stream-start commit
and the <|message|> debris blanket all go away.

One new rule the seed exposed: a channel header can never CONTAIN an ATEM
control token, so a header candidate with a marker before its <|message|>
is voided immediately in both layers -- turn-start prose ahead of the first
real header flows as content instead of vanishing into a giant header, and
the stray-start+junk shapes now resolve mid-stream instead of waiting for
the EOS rule.

Tests per review: the corpus gains the round-5 shapes it existed to police
(stray start+junk, death inside an unfinished header, closing-tag residue)
and its one-shot content check no longer hides behind an always-true
disjunct; the giant-header pin's docstring now names the load-bearing
assertion; new pins cover the three lookalike shapes, seed no-leak, and
prose-before-first-header (each verified to fail under mutation). Re-ran
the NVFP4 checkpoint end-to-end: chat, reasoning split, streaming and
non-streaming tool calls, tool round-trip.
@jason-fxz
jason-fxz merged commit 6223cda into main Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants