Skip to content

feat(qwen4): detect the line and validate its geometry - #1130

Merged
FeathBow merged 16 commits into
pegainfer-project:mainfrom
sysuTobo:feat/qwen38-flash-next-detection
Oct 11, 2026
Merged

FeathBow merged 16 commits into
pegainfer-project:mainfrom
sysuTobo:feat/qwen38-flash-next-detection

Conversation

@sysuTobo

@sysuTobo sysuTobo commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

Refs #1104 — one of three PRs delivering slice A of the #1103 Qwen3.8-Flash-Next bring-up, so this does not close the issue. The tensor contract that enforces this config against real checkpoint bytes follows as a separate PR stacked on this one; the gated-norm activation this line needs is #1129, which is independent of both.

Qwen/Qwen3.8-Flash-Next is a new line, not a Qwen3.5 variant: model_type: qwen4_exp with an inner qwen4_exp_text, against Qwen3.5's qwen3_5. The Qwen3.5 probe hard-requires model_type == qwen3_5, so today this checkpoint surfaces as an unknown model instead of a known-but-uncompiled one, and the server cannot name the feature that would serve it.

Production invariant changed: ModelLineRegistry::detect gains a claimant, and feature_gate_hint in the server binary gains a family. Registration is more than model_line.rs — the server hand-mirrors every line's probe identities in a hint descriptor its own comment says cannot be derived away, so the mirror is tested and not just the probe. detect requires a unique claimant, which is why the routing regression below matters more than it looks.

Detection succeeds and launch refuses with the reason. There is no text graph, no weights loader and no step, so the alternative would be serving a partial model.

Probe

Fail-closed identity plus structure, following the Gemma 4 pattern of pinning structure rather than widths.

Pinned, because an alternative would need a kernel or serving path this line does not have: the GDN head dims (they are the Triton AOT specialization keys), head_dim 256, the conv kernel ceiling, rope_type: default (no YaRN path), output_gate_type: sigmoid, mamba_ssm_dtype: float32, dtype: bfloat16 — so the FP8 sibling fails closed rather than loading as BF16 — and untied embeddings.

Deliberately not pinned: expert count, hidden size, layer count, head counts, hc_count. A smaller qwen4_exp checkpoint should reuse this probe, which is what the L20 small-model deployment case in #1103 needs. smaller_widths_still_pass keeps that promise honest rather than aspirational.

Alongside the pinned values the probe refuses the shapes that are unservable rather than merely different: a num_hidden_layers that disagrees with layer_types, a zero hidden_size or vocab_size, and an ngram_vocab_size_base with no room for a prime search. mrope_interleaved and mrope_section are accepted, not refused: a text-only sequence is their degenerate case in the reference itself, which expands one position per token across all three axes before interleaving, so every rotary pair takes the same position with its own frequency. The decision and the fixtures that would tell it apart are tracked in #1105.

Config

Two fields the reference implementation reads are absent from config.json and come from upstream defaults, so both are modelled explicitly with those defaults: norm_topk_prob (true, and it decides whether the router renormalizes its top-k) and seed. A port that reads only config.json silently drops the router renormalization — this is #1068's mirror image.

The rope values come from the nested object the reference reads (modeling_qwen4_exp.py takes config.rope_parameters["rope_theta"] and [..., "partial_rotary_factor"]), so TextConfig models that copy. The checkpoint also carries a legacy top-level partial_rotary_factor that nothing upstream reads; the probe refuses a checkpoint where the two disagree rather than letting readers pick differently. num_experts_per_tok is carried as well — the probe validates 0 < k <= num_experts exactly as the reference's config does, and a router that re-read config.json on its own would be a second source of truth.

The n-gram table's geometry is derived rather than read, because config.json states none of it:

heads          = (ngram_size - 1) * heads_per_ngram = 2 * 8 = 16
head_dim       = ple_embed_dim / heads              = 2560 / 16 = 160
head i vocab   = the (i+1)-th prime after (ngram_vocab_size_base - 1)
rows per shard = ceil(sum / 128) * 128 / split_ngram_parts = 2,500,012
bytes at BF16  = 102,400,491,520                    = 102.4 GB

102.4 GB, not the ~51 GB vLLM and SGLang quote — that figure is the fp8 footprint and this checkpoint ships BF16. Every host-RAM budget in the issue set was 2× under until it said which dtype it meant.

The derivation lands here rather than with its first consumer because #1106 needs the geometry independently of any tensor contract, and because it is pure arithmetic over config.json; nth_prime_after_matches_the_reference pins the prime search against the reference's _find_nth_prime_after, including its lower bound (searching from 0 must not call 1 prime).

Crate shape

No CUDA dependency at all, so its 44 tests need no device, no weights and no Triton-equipped build. CI runs them on a CPU runner (cpu-unit-tests, cpu-clippy). The per-model CUDA matrix also has a row for this line, for the opposite reason — to compile the server side, which is the one part of this PR that cannot be checked on a CPU-only box (see Evidence).

Preserving that split is the point of the crate's shape: when slice B adds the device side, config and probe must move behind #[cfg(any(feature, test))] the way pegainfer-gemma4/src/lib.rs does, not behind a whole-crate #![cfg(feature)] the way Qwen3.5 does — the latter would make even pure-CPU config tests require a Triton build.

Nothing outside the test suite consumes the geometry yet, so the crate carries #![cfg_attr(not(test), allow(dead_code))], with a comment saying it comes off when the first runtime caller lands. Test builds keep the lint, which is how the width-pinning test below came to exist rather than a blanket allow.

Evidence

  • Routing is unchanged for every existing line. no_other_lines_identity_is_accepted walks 13 foreign model_type values; qwen35_is_not_mis_routed_here pins the specific confusion this line exists to avoid; three #[cfg(not(feature))] tests pin the server's hint mirror (both identities, and that the two lines do not cross-hint), one #[cfg(feature)] test pins that it goes silent once the line is compiled in, and one that it stays silent for an unknown family.
  • The frozen config fixture is the real file, not a transcription: a test re-hashes tests/frozen_config.json against FROZEN_CONFIG_SHA256 at revision de4b8e4d, so the fixture cannot drift from the revision it claims to be. The reference implementation itself first ships in transformers 5.16.0 — the checkpoint's declared 5.8.0.dev0 is what it was authored against, not a release carrying qwen4_exp — and PINNED_TRANSFORMERS names that, with a test holding the two facts apart.
  • A finding the width-pinning test surfaced, which slice B has to plan around: this line's GQA group is 24 / 2 = 12, and SUPPORTED_GQA_GROUP_SIZES is [1, 2, 3, 4, 8]. So decode_group_is_compiled() is false here and captured-graph decode is off the table — the same corner Qwen3.5-27B is in at group 6. Recorded in the doc's Traps rather than left for slice B to discover.
  • The registration chain is compiled by CI now. The line has its own cuda-clippy matrix row — feature: qwen38-flash-next, extra_packages: -p pegainfer-server, needs_triton: false — which is what compiles the registry entry and the hint family, plus a step that runs those tests with the feature both ways. Before that row existed the claim rested on a hand-run on L20 / sm_89: cargo check --release --locked -p pegainfer-server --features qwen38-flash-next --all-targets → exit 0, and the hint tests → 5 passed with the feature on, 7 passed with it off. Note the row only runs once this PR leaves draft: ci.yml gates the heavy jobs on draft == false.
  • CI's exact commands run locally: cargo fmt --all --check, cargo clippy --release --locked -p pegainfer-build -p pegainfer-frontend -p pegainfer-sim -p pegainfer-qwen38-flash-next -p kvbm-logical --all-targets -- -D warnings, cargo test --release --locked -p pegainfer-qwen38-flash-next --lib → 44 passed.

Docs: docs/models/qwen38-flash-next/bring-up.md (freeze table with file hashes, the measured traps, what is still open) plus its docs/index.md row.

What the pre-submission audit changed

An audit of this branch at d3bb63cc found four issues worth fixing and nine worth tightening; 0820991b closes them:

  • the pinned transformers release was 5.8.0, which carries no qwen4_exp — repointed to 5.16.0, pinned by a test;
  • partial_rotary_factor had two sources: the probe validated the nested copy while TextConfig read the legacy top-level one — one source now, and a disagreement is refused;
  • num_experts_per_tok was validated by the probe but never carried — it is a TextConfig field now;
  • the registration path had no CI coverage at all (the feature was enabled by no matrix row) — hence the row above;
  • plus the input-domain checks, the prime search's lower bound, and the mrope reasoning written into the probe. The sigmoid output_gate_type note in launch and in the doc now names feat(kernels): make the gated RMSNorm activation selectable #1129, so neither goes stale when that PR merges.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

Checklist

  • My code follows the style guidelines of this project (see docs/conventions/coding-style.md).
  • I have performed a self-review of my own code.
  • I have formatted my commits according to Commitizen conventions.
  • I have run the local test suite and all tests pass (see CLAUDE.md).

Test plan

  • CI green for pegainfer-qwen38-flash-next: cpu-unit-tests, cpu-clippy, and the new Qwen3.8-Flash-Next CUDA Clippy row (the last one needs this PR out of draft).
  • cargo run --release -- --model-path <Qwen3.8-Flash-Next> on a stock build should print the --features qwen38-flash-next hint rather than reporting an unknown model.
  • The same with --features qwen38-flash-next should detect the line and then refuse to launch, naming slice B as the reason.
  • On a CUDA-capable box (L20 / sm_89), before the CI row existed: cargo check --release --locked -p pegainfer-server --features qwen38-flash-next --all-targets → exit 0, and cargo test --release --locked -p pegainfer-server --features qwen38-flash-next --bin pegainfer hint → 5 passed; the same without the feature → 7 passed.

@github-actions github-actions Bot added the enhancement New feature or request label Oct 5, 2026
@sysuTobo
sysuTobo marked this pull request as draft October 5, 2026 03:07
@sysuTobo
sysuTobo force-pushed the feat/qwen38-flash-next-detection branch from 0820991 to 400d9d4 Compare October 6, 2026 17:38
@sysuTobo

sysuTobo commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Code audit — round 2 (head 400d9d4e, base c8a09c9d)

TL;DR: the forward paths all check out, and this round every load-bearing number was traced to real bytes: the committed fixture is byte-identical to the upstream config.json at de4b8e4d; the n-gram prime derivation matches the checkpoint's stored ngram_heads_vocab_sizes / ngram_heads_offsets tensors (I64[16], range-fetched from model-00037-of-00131.safetensors) byte for byte; shard_127.weight really is [2500012, 160] BF16, pinning rows-per-shard, head_dim and the 102.4 GB figure at once. 44/44 tests reproduced locally. Of round 1's 13 findings, 12 are fixed and 1 (R9) was fixed but went stale again when the branch was rebased onto the #1129 merge. New this round: 9 findings (5 M / 4 L), mostly one theme — the probe's mirror of the reference's own validate_architecture is incomplete — each verified line-by-line against the transformers v5.16.0 sources.

New findings

ID Sev Finding Evidence
N1 M The probe comment still says the in-tree gated norm "hardcodes silu … until that lands" — #1129 (this PR's own base) landed the sigmoid entry point probe.rs:228-232 vs pegainfer-kernels/csrc/shared/norm.cu:7-9,83 at base
N2 M Same staleness in the doc: "has to become selectable … that change is a separate PR (#1129)" — #1129 is merged in base bring-up.md:107-113
N3 M NgramTable::derive hardcodes ple_layer_index = 0; the reference primes head i of PLE layer L at global index L * ngram_heads + i, so a multi-PLE-layer checkpoint passes the probe and derives wrong geometry for layer ≥ 1 config.rs:282-287 vs modeling_qwen4_exp.py:1037-1039
N4 M Probe omits the reference's hc_count > 1 rule (a single-branch residual stream is refused upstream) probe.rs:284-292 vs configuration_qwen4_exp.py:196
N5 M Probe omits three reference QSA rules: indexer_kv_heads == 1, indexer_budget % indexer_compress_ratio == 0, rotary_dim <= indexer_head_dim probe.rs:241-253, :417-426 vs configuration_qwen4_exp.py:221,223,227
N7 M rope_theta is read by the reference from the same nested object, by direct index (rope_parameters["rope_theta"]), yet is neither probed nor carried — while RopeParameters' doc claims to be "the only canonical copy" of "every rope value". A slice-B rope consumer would have to re-read config.json: the exact second-source shape R3 was fixed for config.rs:42-53 vs modeling_qwen4_exp.py:108
N6 L Probe omits "PLE only on linear_attention layers" probe.rs:298-317 vs configuration_qwen4_exp.py:248-254
N8 L Labeler name/globs say qwen4 while the line/crate/feature/docs-dir are all qwen38-flash-next; kernel files named after the line (*qwen38*) would not get labelled .github/labeler.yml:18-25
N9 L no_other_lines_identity_is_accepted's docstring says the other half ("no existing line claims qwen4_exp") is pinned by the server's hint tests — hint tests pin the hint table; identity uniqueness rests on each line's own hard gate plus the registry's Conflict probe.rs:471-477 vs pegainfer-qwen35/src/model_line.rs:85

Report-only (no code change proposed): rotary_dim()'s bail is unreachable after probe_rope (defensive D1, fine); ngram_vocab_size_base has no upper bound so a pathological config could make the sieve allocate ~4.3 GB (the reference is equally unbounded and config.json is operator-controlled); the CI hint substring filter is naming-fragile (its "every test in that module" comment is true today — checked); smaller_widths_still_pass exercises the probe but not Config::from_json; the reference also requires eos_token_id when PLE is on (configuration:255-256) and rewrites full_attention → qwen_sparse_attention in layer_types (:181-185) — both are slice-B/#1105 territory; the PR body's "Qwen3.5 probe hard-requires qwen3_5" is actually outer-or-text identity.

Round-1 receipts (R1–R13)

ID Status
R1 Fixed — PINNED_TRANSFORMERS = "5.16.0" + the two-facts test. Re-verified independently: qwen4_exp/ is 404 at tags v5.8.0 and v5.15.0, present at v5.16.0
R2 Fixed (better than the prescription) — nested rope_parameters is canonical, disagreement refused, dedicated test
R3 Fixed — num_experts_per_tok carried; the cited configuration_qwen4_exp.py:200 line number is accurate (checked)
R4 Fixed — pointer comment in the probe, no vacuous assertion, as prescribed
R5 Fixed — CUDA matrix row + "both ways" test step; the 5-on/7-off counts re-derived by hand and correct
R6 Documented bound (correct to keep) — allow(dead_code) with its removal condition; slice B must delete it
R7 Fixed — the empty-feature comment in the crate's Cargo.toml
R8 Fixed — CLAUDE.md row now says "Foundation only (probe, config)"
R9 Fixed-then-staled — the fix pre-dated #1129 merging; the rebase onto c8a09c9d inverted its tense ⇒ re-fixed as N1/N2
R10–R13 Fixed — layer-count cross-check, hint test coverage, prime lower bound (double: max(1) + base >= 2 probe rule), zero width/vocab refusals; each has its dedicated test

Byte-level verification performed this round

  • fixture ≡ upstream config.json @ de4b8e4d (diff = identical; sha256 889658f2… = FROZEN_CONFIG_SHA256 = upstream file hash)
  • model.safetensors.index.json: 1658 keys, 131 files, total_size 359,999,963,128 B = 360.0 GB — freeze table fully confirmed
  • lm_head.weight [248320, 2560] lives alone in model-00131; embed_tokens in model-00130 — untied ✓; the only model-level norm is hyper_connection_mixer.hc_norm.weight — "no final-norm tensor" ✓
  • PLE tensors exist only under layers.1 — the 1-indexed [2] trap confirmed against the real layout ✓
  • ngram_embedding.shard_0…shard_127 — exactly 128 named shards ✓; shard_127.weight = [2500012, 160] BF16 ✓
  • stored ngram_heads_vocab_sizes = [20000003, 20000023, …, 20000171] and ngram_heads_offsets = the derived prefix sums — byte-exact against an independent recomputation ✓
  • ple.conv1d.weight = [10240, 1, 4], same shape as the GDN conv — the "identical shape, dilated anyway" trap confirmed; reference conv_dilation = ngram_size, state (4-1)*3 = 9 (modeling:1134-1135) matches ple_conv_state_len() ✓
  • _find_nth_prime_after (modeling:1009-1015) semantics + _is_prime(<2) = False — the port's start.max(1) floor is equivalent on {0,1}; all 7 pinned values recomputed ✓; padding formula ≡ modeling:1052-1053 ✓
  • norm_topk_prob: bool = True / seed: int = 1234 upstream defaults (configuration:156,163), consumed by the router (modeling:903,912), absent from the fixture ✓
  • mrope degeneration argument holds: 2-D positions expand(3, …) (modeling:123-124) then the interleaved overwrite copies identical axes onto themselves (:140-156) = plain 1-D rope ✓
  • GQA group 12 ∉ SUPPORTED_GQA_GROUP_SIZES = [1,2,3,4,8] (pegainfer-kernels/src/ops/attention.rs:18) ✓; gemma4 cfg(any(feature, test)) precedent ✓; load_shard_info_fixed exists (pegainfer-core/src/weight_loader.rs:1055) ✓; reasoning_effort arms on output_gate_type (pegainfer-frontend/src/vllm/reasoning_effort.rs:16,94) ✓
  • referenced issues all exist and match: [Model] Qwen3.8-Flash-Next roadmap #1103 roadmap / [Model] Qwen3.8-Flash-Next: line skeleton & bring-up #1104 slice A / [Model] Qwen3.8-Flash-Next: QSA sparse attention #1105 QSA / [Model] Qwen3.8-Flash-Next: text-graph assembly & decode step #1127 slice B / [Model] Qwen3.8-Flash-Next: multi-GPU EP serving & envelope #1128 slice C ✓
  • cargo test --release -p pegainfer-qwen38-flash-next --lib → 44 passed locally ✓

Verdict

Detection, fail-closed refusals, geometry derivation and the registration/hint surface are all correct for the frozen checkpoint and for the tested variants. The real gap is that the probe is looser than the reference's own validation in five places (N3–N7): today none of them is reachable from the only checkpoint that exists, but each one turns "known-but-uncompiled" into "known, detected, and silently mis-derived" for a future qwen4_exp variant — which is precisely the class of failure this crate exists to prevent. All nine findings are mechanical to close; a fix commit follows on this branch.

@sysuTobo

sysuTobo commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Fix commit for the round-2 findings: 8e0ec018

Every code-level finding from the audit above is closed on the branch; each fix carries a dedicated test. Mapping:

Finding Fix Guard
N1 (M) stale "hardcodes silu" comment probe.rs output-gate comment rewritten: #1129 made the shared kernel selectable (rms_norm_gated_sigmoid_cuda beside Qwen3.5's SiLU); what remains is this line's call site. The sigmoid pin stays fail-closed — no silu-gated qwen4_exp variant has golden fixtures here existing a_non_sigmoid_output_gate_bails (rationale now accurate)
N2 (M) stale doc trap bring-up.md Traps now reads "became selectable in #1129 … merged before this line's detection PR, so what remains is this line's call site" — (doc; tense anchored to a merged commit)
N3 (M) multi-PLE-layer mis-derivation probe_ple refuses any ple_layer_ids but a single entry, naming the reference's per-layer prime offset (global_head_idx = L * ngram_heads + i, modeling:1037-1039); NgramTable's doc now states the single-layer assumption a_multi_layer_ple_bails ([2, 6] refused)
N4 (M) hc_count > 1 missing probe_hyper_connections mirrors configuration:196 — a single-branch residual stream has nothing to mix a_single_branch_residual_stream_bails
N5 (M) QSA rules missing probe_attention mirrors indexer_kv_heads == 1 (:221) and budget a whole multiple of the compress ratio (:223); probe_rope mirrors rotary ≤ indexer_head_dim (:227) an_indexer_kv_heads_above_one_bails, an_indexer_budget_not_divisible_by_the_compress_ratio_bails, a_rotary_width_past_the_indexer_head_bails
N6 (L) PLE-on-linear rule missing probe_ple mirrors configuration:248-254 (safe subscript: probe_layer_types runs first and pins layer_types length to num_hidden_layers) ple_on_a_full_attention_layer_bails ([4] is full under interval 4)
N7 (M) rope_theta unmodelled Carried as a required RopeParameters field (serde-required ⇒ absence refuses at parse), probed positive-and-finite (the reference direct-indexes it, modeling:108) — no consumer can need a second read of config.json a_missing_rope_theta_bails + bit-exact pin 10_000_000.0 in the_frozen_revision_pins_the_attention_and_moe_widths
N8 (L) labeler naming gap Added *qwen38* kernel globs beside *qwen4*, so files named after the line label the same as files named after the model_type — (labeler only)
N9 (L) docstring evidence pointer Reworded: identity uniqueness rests on each line's own hard gate + the registry's Conflict; the server's hint tests pin that the hint table does not cross-map — (docstring)

Report-only items stay report-only by design: rotary_dim()'s defensive bail (unreachable after the probe, kept), the unbounded ngram_vocab_size_base sieve window (the reference is equally unbounded and config.json is operator-controlled), the CI hint filter's naming fragility (its comment is true today), smaller_widths_still_pass covering the probe but not from_json, eos_token_id and the qwen_sparse_attention rename (slice B / #1105 territory), and the allow(dead_code) bound (R6, comes off with the first runtime caller).

Verification

  • cargo test --release --locked -p pegainfer-qwen38-flash-next --lib → 51 passed (44 + 7 new; the description's "44 tests" and the doc's count are updated where they live — the description itself can't be edited, so noting it here)
  • cargo clippy --release --locked -p pegainfer-qwen38-flash-next --all-targets -- -D warnings → clean (exit 0; the only output is the pre-existing third-party proc-macro-error2 future-incompat notice)
  • cargo fmt --all --check → clean
  • The frozen config still passes end-to-end (the_frozen_revision_passes, the_registry_entry_detects_the_frozen_revision), and smaller_widths_still_pass still holds — the new rules mirror the reference's own refusals, so no config the reference accepts is rejected here
  • Server-side hint tests are untouched by this commit (no main.rs change)

Round-1 receipts stand as tabulated in the audit comment: R1–R5, R7–R8, R10–R13 fixed; R6 a correctly documented bound; R9 fixed-then-staled-by-rebase and now re-fixed as N1/N2.

@sysuTobo
sysuTobo marked this pull request as ready for review October 8, 2026 14:39
@sysuTobo
sysuTobo marked this pull request as draft October 8, 2026 15:16
@sysuTobo
sysuTobo marked this pull request as ready for review October 9, 2026 01:58
@sysuTobo
sysuTobo force-pushed the feat/qwen38-flash-next-detection branch from 8e0ec01 to 6294278 Compare October 9, 2026 02:13
@github-actions github-actions Bot added the needs-rebase Conflicts with main, needs a rebase label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main. Please rebase it so it can be reviewed and merged.

@sysuTobo
sysuTobo force-pushed the feat/qwen38-flash-next-detection branch from 6294278 to 4ea9ef4 Compare October 9, 2026 09:03
@github-actions github-actions Bot removed the needs-rebase Conflicts with main, needs a rebase label Oct 9, 2026
/// The `count`-th prime strictly greater than `start`. Mirrors the reference's
/// `_find_nth_prime_after`; the search window is tiny (a few hundred candidates
/// for a base near 2e7), so trial division by sieved small primes is enough.
fn nth_prime_after(start: usize, count: usize) -> usize {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

每次call这个函数都得重算线性筛,是否太耗时?如果调用次数少,是否应该打表;如果次数多;是否应该预处理?

@sysuTobo

sysuTobo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Round 3: the registration follow-ups, and a prime-sieve fix

Two commits on the branch since the last round (both on top of the rebase onto main @ 67bd8a2b, which also closed the merge-conflict notice above — the PR is MERGEABLE/CLEAN).

61dd6779 — three follow-ups from reviewing the registration surface

ID Finding Fix
G1 launch's refusal framed the GDN gated-norm activation as an open kernel dependency, but #1129 already made the sigmoid entry point selectable. The message now says the missing piece is the call site, which lands with the text graph in slice B. pegainfer-qwen38-flash-next/src/model_line.rs:27-34
G2 feature_gate_hint is identity-only — it cannot run a compiled-out probe — so it also fires for the FP8 sibling and the tied/YaRN variants of qwen4_exp, which the probe refuses. That was undocumented. Written down on the family table (pegainfer-server/src/main.rs:54-67) and in bring-up.md.
G3 The hint table is a hand-mirror of each line's identity, and nothing bound it to the probe. families() is now reachable from a test, and a feature-gated test asserts the qwen38-flash-next row equals the line crate's MODEL_TYPE / TEXT_MODEL_TYPE (main.rs:386-402). Those constants move the identity strings to one place (probe.rs:26-35, re-exported by lib.rs:31-32).

346f6445 — nth_prime_after re-sieved on every candidate

The lazy re-sieve compared the candidate's square root against the largest prime in the current sieve, not against the sieve's limit. Near the frozen base that root is 4473 = 3²·7·71 — composite — so the test held for every candidate.

Measured (x86_64, -O) on the derivation one Config::from_json performs — heads = (ngram_size − 1) × heads_per_ngram = 16 calls of nth_prime_after:

variant per Config::from_json
before 6.26 ms (the last call alone re-sieved 172 times)
after (compare against the sieve limit) 175 µs
single linear sweep for all 16 primes (rejected) 17.5 µs

Derived values are unchanged; nth_prime_after_matches_the_reference and the n-gram geometry tests pin them.

Why neither a precomputed table nor a shared pre-pass:

  • NgramTable is derived on purpose — the checkpoint stores its own ngram_heads_vocab_sizes, and deriving is exactly what lets the manifest reject a partition that disagrees with its own config before any bytes move.
  • ngram_vocab_size_base and heads are config values the probe deliberately does not pin (ngram_vocab_size_base ≥ 2, any integer). A hard-coded table would be a second source of truth that cannot enumerate a base it permits.
  • The remaining 10× (175 µs → 17.5 µs) would cost the 1:1 correspondence with the reference's _find_nth_prime_after, which the unit test reads directly. Not worth it on a once-per-load cold path.

Verification

  • Local: cargo fmt --all --check ✓, 51 line-crate tests ✓, cargo clippy -D warnings on the crate ✓.
  • CI run 37944431030 (head 61dd6779): 16/16 jobs green, including the Flash-Next row's both-ways step — 6 hint tests with the feature on (the new binding test among them) and 7 with it off.
  • CI on 346f6445 is running.

@FeathBow

Copy link
Copy Markdown
Collaborator

@codex review please.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T09:36:28.126485Z 346f644 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 346f6445d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

fn probe(&self, config: &serde_json::Value) -> Result<(), String> {
crate::probe::probe_config_json(config).map_err(|error| error.to_string())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run geometry derivation from the registered probe

For a qwen4_exp config with positive but inconsistent derived geometry (for example, ple_embed_dim = 2561 or split_ngram_parts = 3), this production method calls only probe_config_json, which accepts it; repo-wide, Config::from_json is called only by tests, where NgramTable::derive rejects those cases. Consequently ModelLineRegistry::detect claims the config and launch says it was validated even though the advertised geometry validation is dead scaffolding; construct Config here (or move its checks into the probe) so the config layer participates in the production invariant.

AGENTS.md reference: AGENTS.md:L128-L128

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1d2699: ModelLine::probe now builds the whole Config, so detect runs the n-gram/head-group derivations too, not just the structural probe. Guarded by the_probe_rejects_geometry_that_does_not_derive (ple_embed_dim = 2561).

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +368 to +370
cargo test --release --locked -p pegainfer-server \
--features qwen38-flash-next --bin pegainfer hint
cargo test --release --locked -p pegainfer-server --bin pegainfer hint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exercise the line gate with a production E2E

In the inspected cuda-clippy job, the new registration step only runs the hint unit-test filter with the feature on and off, and a repo-wide qwen4_exp search finds no test that invokes the built server. This feature changes production detection and the intentional launch-refusal path, so these pure tests do not provide the repository-required production E2E or cover the CLI/registry/startup wiring as shipped; add a release-binary test against a model directory that asserts both the stock-build feature hint and the enabled-build refusal.

AGENTS.md reference: AGENTS.md:L128-L128

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1d2699: pegainfer-server/tests/detect_gate.rs runs the shipped binary against a model dir holding the frozen config, both ways — the stock build must name --features qwen38-flash-next, the enabled build must detect and refuse. Wired into the CUDA row; both cfgs green.

Comment on lines +201 to +205
for field in ["num_attention_heads", "num_key_value_heads"] {
let v = text_config.get(field).and_then(serde_json::Value::as_u64);
if v.is_none_or(|v| v == 0) {
bail!("qwen4_exp: {field} must be a positive integer, got {v:?}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-integral full-attention head groups

For a smaller checkpoint with positive but non-divisible head counts (for example, 10 query heads and 3 KV heads), this loop accepts the config even though grouped attention needs an integral query/KV ratio. The analogous Qwen3.5 validation rejects this at pegainfer-qwen35/src/config/model.rs:265-271, and the shared attention dispatch operates on that derived group, so this probe can claim a geometry the planned attention path cannot represent; require num_attention_heads to be a multiple of num_key_value_heads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1d2699: the probe now rejects a non-integral query/KV ratio, mirroring qwen35's AttentionHeadDivisibility. Test: a_non_integral_gqa_group_bails (10 query / 3 KV heads).

Comment on lines +221 to +225
for field in ["linear_num_key_heads", "linear_num_value_heads"] {
let v = text_config.get(field).and_then(serde_json::Value::as_u64);
if v.is_none_or(|v| v == 0) {
bail!("qwen4_exp: {field} must be a positive integer, got {v:?}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-integral GDN head groups

For a smaller checkpoint whose positive linear value-head count is not a multiple of its key-head count, this loop accepts the config even though Gated DeltaNet maps value heads in whole groups over key heads. The reused Qwen3.5 geometry enforces this exact invariant at pegainfer-qwen35/src/config/model.rs:255-263; without the same check here, detection can validate a shape the shared GDN path cannot represent, so reject unless linear_num_value_heads is divisible by linear_num_key_heads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1d2699: the probe now requires linear_num_value_heads % linear_num_key_heads == 0, mirroring qwen35's LinearHeadDivisibility. Test: a_non_integral_gdn_head_group_bails (48 value / 10 key heads).

@github-actions github-actions Bot added the needs-rebase Conflicts with main, needs a rebase label Oct 11, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts with main. Please rebase it so it can be reviewed and merged.

Qwen3.8-Flash-Next is a new line, not a Qwen3.5 variant: its model_type is
qwen4_exp with an inner qwen4_exp_text, so the Qwen3.5 probe refuses it by
construction and an unguarded default build would report an unknown model
rather than naming the feature that serves it.

Land the foundation the rest of the line stands on: a fail-closed probe, a
config layer that validates the geometry, and the registration that lets the
server detect the checkpoint and refuse to launch it. There is no text graph,
so launch bails with the reason instead of serving a partial model.

The probe pins structure rather than widths, following the Gemma 4 pattern
that lets one crate serve several sizes. Pinned are the things an alternative
would need a kernel or serving path this line does not have: the GDN head dims
(they are the Triton AOT specialization keys), head_dim 256, the conv kernel
ceiling, rope_type default, output_gate_type sigmoid, mamba_ssm_dtype float32,
dtype bfloat16 so the FP8 sibling fails closed rather than loading as BF16, and
untied embeddings. Deliberately unpinned are expert count, hidden size, layer
count, head counts and hc_count, so a smaller qwen4_exp checkpoint reuses this
probe; smaller_widths_still_pass keeps that promise honest.

Two fields the reference implementation reads are absent from config.json and
come from upstream defaults, so they are modelled explicitly: norm_topk_prob
decides whether the router renormalizes its top-k, and a port that reads only
config.json silently drops that. This is pegainfer-project#1068's mirror image.

The crate carries no CUDA dependency at all, so its 38 tests need no device, no
weights and no Triton-equipped build, and CI runs them on a CPU runner rather
than in the per-model CUDA matrix. The server binary hand-mirrors every line's
probe identities in a hint descriptor its own comment says cannot be derived
away, so the mirror is tested and not just the probe.

Refs pegainfer-project#1104

Signed-off-by: bohaotu <bohaotu@pony.ai>
The freeze table read the checkpoint's declared `5.8.0.dev0` as the release that
carries the reference implementation. It is not: `src/transformers/models/qwen4_exp/`
is absent from tags v5.8.0 through v5.15.0 and present from v5.16.0, so 5.16.0 is
the first release that can serve as a reference.

The tokenizer row gets the same correction for a different reason: the checkpoint's
own tokenizer is `Qwen2Tokenizer`, which is much older, but the fixtures are
compared against the reference implementation.

Signed-off-by: bohaotu <bohaotu@pony.ai>
An audit of this PR found four issues that matter and nine worth tightening, all
in the gap between what the gate claims and what it guards.

- The reference implementation first ships in transformers **5.16.0**, not the
  `5.8.0` the constant pinned: the same conflation of the checkpoint's declared
  `5.8.0.dev0` with the release that carries the reference that the doc fix
  corrected. Repointed, and pinned by a test that holds both facts apart.
- `partial_rotary_factor` had two sources: the probe validated the nested
  `rope_parameters` copy while `TextConfig` read the legacy top-level one. The
  reference reads the nested object and nothing reads the top-level copy, so the
  config now models the nested one, and the probe refuses a checkpoint whose two
  copies disagree rather than letting readers pick differently.
- `num_experts_per_tok` was validated by the probe but never carried, so a router
  would have had to re-read `config.json` — a second source of truth. It is a
  `TextConfig` field now, and the check has a consumer.
- The registration path had no CI coverage at all: `pegainfer-server` gates its
  registry entry on `qwen38-flash-next`, no matrix row enabled that feature, so
  the entry never compiled and the hint tests never ran. The line gets its own
  row, plus a step that runs those tests with the feature on and off.

Also tightened: `num_hidden_layers` must agree with `layer_types`,
`hidden_size`/`vocab_size` must be positive, and `ngram_vocab_size_base` must
leave room for a prime search — the helper accepted 1 as prime when searching
from 0. The probe now records why `mrope_interleaved`/`mrope_section` are
accepted rather than refused: a text-only sequence is their degenerate case in
the reference, which expands one position across all three axes before
interleaving. The decision is tracked in pegainfer-project#1105.

Signed-off-by: bohaotu <bohaotu@pony.ai>
…e probe

A round-2 audit of this branch found the probe looser than the reference
implementation's own validate_architecture, plus two notes that went stale
when the branch was rebased onto the pegainfer-project#1129 merge:

- The reference primes head i of PLE layer L at the global index
  L * ngram_heads + i (modeling_qwen4_exp.py:1037-1039), while
  NgramTable::derive fixes the layer index at 0; a multi-PLE-layer
  checkpoint would pass detection and derive the wrong per-layer
  geometry, so the probe now refuses any ple_layer_ids but a single
  entry until per-layer tables are modelled.
- The reference refuses hc_count <= 1 (configuration_qwen4_exp.py:196),
  indexer_kv_heads != 1 (:221), a budget that is not a whole multiple of
  the compress ratio (:223), a rotary width past the index head (:227),
  and PLE on a non-linear-attention layer (:248-254); the probe now
  mirrors all five, since a config the reference refuses cannot be
  served by any port of it.
- The reference indexes rope_parameters["rope_theta"] directly
  (modeling_qwen4_exp.py:108), but TextConfig did not carry it, leaving
  a future rope consumer to re-read config.json — the second-source
  shape the previous audit removed for num_experts_per_tok. It is now
  modelled, probed, and pinned bit-exactly.
- pegainfer-project#1129 merged before this branch's base, so "the in-tree gated norm
  hardcodes silu until that lands" went stale in the probe comment and
  in the bring-up doc; both now record that the kernel is selectable
  and only this line's call site is outstanding.
- The labeler now also matches kernels named after the line (*qwen38*),
  not only after the model_type (*qwen4*).
- The identity-uniqueness test's docstring credited the server's hint
  tests with pinning other lines' probes; reworded to name the actual
  guards (each line's hard identity gate plus the registry's Conflict).

Seven new probe tests and one new config pin: 44 -> 51, all passing on
CPU. The derivation was re-verified against real checkpoint bytes: the
stored ngram_heads_vocab_sizes / ngram_heads_offsets tensors
(model-00037-of-00131) match the derived prime run byte for byte, and
shard_127 is [2500012, 160] BF16 exactly as derived.

Signed-off-by: bohaotu <bohaotu@pony.ai>
…d against

The row's "Run the registration tests both ways" step is the first thing in
this repo's CI that links a CUDA-linked binary: `cargo clippy` never links, so
the row passed until the PR left draft, and then the test binary failed with
`rust-lld: error: unable to find library -lcuda` — pegainfer-kernels' build
script asks for it unconditionally, and a GitHub runner has no driver.

Install `driver-dev` in the shared CUDA toolkit step and point the linker at
its stub through LIBRARY_PATH, the exact pairing
`.github/workflows/release.yml` already uses to build the server on a
driver-less runner, plus a copied `libcuda.so.1` on LD_LIBRARY_PATH so the
loader can resolve the stub's SONAME when the test binary runs. The hint tests
never call into the driver, so the stub is sufficient and no root is needed.

Signed-off-by: bohaotu <bohaotu@pony.ai>
…ale note

Three follow-ups from the line's review:

- `launch`'s refusal no longer frames the GDN gated-norm activation as an
  open kernel dependency: pegainfer-project#1129 made the sigmoid entry point selectable, so
  the missing piece is the call site, which lands with the text graph.
- `feature_gate_hint`'s identity table is documented as identity-only — it
  cannot run a compiled-out probe, so it also fires for the FP8/tied/YaRN
  variants the probe refuses.
- `families()` becomes reachable from a test, and a feature-gated test binds
  its `qwen38-flash-next` row to the line crate's `MODEL_TYPE` /
  `TEXT_MODEL_TYPE`, so the hand-mirror cannot drift from the probe.

Signed-off-by: bohaotu <bohaotu@pony.ai>
`nth_prime_after` re-sieved whenever the candidate's square root exceeded the
*largest prime in* the current sieve. Near the frozen base that root is
4473 = 3^2 * 7 * 71, composite, so the test held for every candidate: the
16-prime derivation for one `Config::from_json` re-sieved ~170 times, 6.26 ms,
against 175 us when the comparison is against the sieve's limit instead.
Derived values are unchanged; the existing reference test pins them.

Signed-off-by: bohaotu <bohaotu@pony.ai>
… gate

Three findings against the shipped gate, all reproduced:

- `ModelLine::probe` called `probe_config_json` alone, so `ModelLineRegistry::detect`
  claimed a config whose *derived* geometry cannot be laid out — `ple_embed_dim = 2561`
  or `split_ngram_parts = 3` pass the structural probe and only `Config::from_json`
  rejects them, and that was test-only. `probe` now builds the `Config`, so the n-gram
  and head-group derivations are on the detection path the line actually runs.
- The probe accepted a non-integral grouped-attention ratio (10 query / 3 KV heads) and
  a non-integral GatedDeltaNet head group (48 value / 10 key heads). Both are invariants
  the shared Qwen3.5 geometry enforces (`AttentionHeadDivisibility`,
  `LinearHeadDivisibility`) and the dispatch is built on; reject them here as well.
- Nothing drove the shipped binary, so `main`'s CLI -> registry -> hint -> `launch`
  wiring had no coverage. `pegainfer-server/tests/detect_gate.rs` runs it against a model
  directory holding the frozen config, both ways: the stock build must name the feature
  to rebuild with, the enabled build must detect the line and refuse to serve. Neither
  outcome touches CUDA, and the CUDA row now runs the test alongside the hint tests.

Signed-off-by: bohaotu <bohaotu@pony.ai>
@sysuTobo
sysuTobo force-pushed the feat/qwen38-flash-next-detection branch from f1d2699 to 1eaae79 Compare October 11, 2026 14:44
@github-actions github-actions Bot removed the needs-rebase Conflicts with main, needs a rebase label Oct 11, 2026
The enabled build reaches the serve path before `launch` bails, and the frontend
refuses to start without a tokenizer, so the test saw "failed to create chat/text
backends" instead of the refusal. The model directory now also carries the repo's
minimal tokenizer (`pegainfer-frontend/examples/echo-model`, the fixture
`examples/echo-server.rs` serves); the engine failure then cancels the server and
the refusal is what reaches stderr.

Signed-off-by: bohaotu <bohaotu@pony.ai>
@FeathBow

Copy link
Copy Markdown
Collaborator

@codex review please.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
Signed-off-by: Feathbow <feathbow@gmail.com>
@github-actions

Copy link
Copy Markdown

Commit attribution notice

This pull request contains commits attributed to an account other than the PR author @sysuTobo without a verified signature from that account:

If you are mentioned above and did not take part in this pull request, please say so here. Attributing a commit to someone else normally requires that person to be involved; maintainers should treat unacknowledged attribution as a review blocker.

This check never fails the build.

@FeathBow

Copy link
Copy Markdown
Collaborator

We pushed seven commits on top of yours.

  • Name. The crate, feature, hint family and docs are now qwen4 (pegainfer-qwen4, --features qwen4, docs/models/qwen4/). The line claims any qwen4_exp checkpoint, the model card calls it "an experimental preview of the architecture that will underpin Qwen4", and the issues and the label already use qwen4. Qwen3.8-Flash-Next is its first checkpoint. The empty crate feature is gone, and the server feature only enables the dependency, as glm52 and k3 do.
  • One typed validation. The probe checks identity only, as Qwen3.5's does. Every other rule moved into config.rs behind one serde parse (RawConfig into Config), so each field is read once and NgramTable::derive no longer repeats checks the probe already made. ModelLine::probe still builds the whole Config, so detection refuses an unservable config before the frontend starts. The rules and their messages carried over.
  • Quantized siblings. The official FP8 config differs from the BF16 one only by a top level quantization_config, and its text_config.dtype is still bfloat16, so the dtype pin let it through. nvidia's NVFP4 config is the same case. A quantization_config at either level is now refused, and the FP8 test uses that real block.
  • Rotary width. An odd width is refused. The reference builds arange(0, rotary_dim, 2) and rotates cos.shape[-1] dims, which is one more than an odd rotary_dim.
  • Prime search. It is now a direct port of the reference's _is_prime and _find_nth_prime_after. The 16 primes and the table geometry are unchanged, and the crate's tests finish in 0.01 s.
  • Dead code. The crate wide allow(dead_code) is gone. Accessors and exports with no caller (hc_hidden_size, the gdn_* widths, ple_conv_state_len, bf16_bytes, seed, FROZEN_REVISION, PINNED_TRANSFORMERS) are removed with their tests, and the revision and the transformers release stay in bring-up.md. The fields the tensor contract will read keep a cfg_attr(not(test), allow(dead_code)), so test builds still check them.
  • Tests. Tests that repeat another one are dropped. In the crate these are two that check serde's required field handling, two pairs that hit the same comparison, the trait object coercion the server registration already compiles, and the wiring test detect_gate covers. In the server, four hint tests are covered by the generic hint tests, the test that binds the qwen4 row to the crate's constants, and detect_gate.
  • Detection gate. Each run of the binary is bounded at 60 s and fails with the run's stderr, where output() could wait forever on a startup or shutdown path that never returns.
  • Launch. The refusal says the checkpoint is detected and inference is not implemented yet. The kernel history stays in bring-up.md.
  • CI and the model table. The CI row, the CPU job entries and the row in the supported models table are dropped until the line serves. detect_gate stays and runs with cargo test -p pegainfer-server.

Checked on one sm_89 box. cargo fmt --all --check, CI's CPU Clippy, Clippy with -D warnings on pegainfer-qwen4 and on pegainfer-server with and without --features qwen4, the crate's unit tests (40 passed), and the server's hint tests plus detect_gate both ways (4 and 1 without the feature, 5 and 1 with it) all pass. With the run budget cut to 1 ms, detect_gate fails with "the binary did not exit within 1ms". The binary against the three real configs:

config stock build --features qwen4
BF16, the frozen revision names --features qwen4 detected, then refuses because inference is not implemented
Qwen/Qwen3.8-Flash-Next-FP8 names --features qwen4 not claimed, quantization_config is present
nvidia/Qwen3.8-Flash-Next-NVFP4 names --features qwen4 not claimed, quantization_config is present

#1104, #1127 and #1128 still use the old name, and a branch stacked on this one needs a rebase over the rename. Tell us if anything here should go another way.

@FeathBow FeathBow changed the title feat(qwen38-flash-next): detect the line and validate its geometry feat(qwen4): detect the line and validate its geometry Oct 11, 2026
@FeathBow
FeathBow merged commit 4b1472e into pegainfer-project:main Oct 11, 2026
15 checks passed
sysuTobo added a commit to sysuTobo/pegainfer that referenced this pull request Oct 12, 2026
An audit of this PR found two issues that matter and six worth tightening; all
eight sit in the gap between what the contract claims and what it makes
verifiable. None of them changes a derived number.

- The ignored gate printed how many tensors it opened but not how many of them
  the contract requires, so the "446 opened / 111 required" figure in the
  bring-up doc could not be reproduced from the gate's own output. It prints the
  required/skipped split now.
- The fault message named "Qwen3.8-Flash-Next" while this line's probe and config
  deliberately serve *any* smaller `qwen4_exp` checkpoint, so a failure on a
  second checkpoint would name the wrong model. It names the architecture
  (`MODEL_TYPE`) now.
- `dtype_size` existed twice and the two disagreed: the production copy returned
  `0` for an unknown dtype, silently shrinking `weight_bytes`, while the test copy
  panicked. There is one copy in `manifest/mod.rs` now, and it panics for both.
- `schema.rs` guarded duplicate derivations with `debug_assert!`, which the
  `--release` profile these tests run under compiles out. It is an `assert!` now.
- The three `dead_code` allows still said "the tensor contract is the first
  reader", which this PR makes stale; they name the actual first reader (slice B's
  loader, and the contract test for `bf16_bytes`).
- Two doc sentences over-claimed: "every probe, config and manifest test runs
  against it" (two of them build their own input) and "three that read real shard
  bytes" (one reads the index).

Not addressed, by decision: the line is not wired into CI (pegainfer-project#1130 kept it out until
slice B), so these tests still run by hand only. Recorded in the PR description as
a known bound rather than fixed here.

Signed-off-by: bohaotu <bohaotu@pony.ai>
sysuTobo added a commit to sysuTobo/pegainfer that referenced this pull request Oct 12, 2026
The detection PR (pegainfer-project#1130) landed the probe and the validated config, so the
checkpoint's tensors were implied by the geometry but never named. This lands the
contract that says exactly which tensors the line must read, and holds the
checkpoint's own headers against it before any H2D.

Ported from `pegainfer-gemma4/src/manifest/`, with three deltas this checkpoint
forces. The LM head is **untied**, so both ends are required and there is no
final-norm tensor — the readout is the hyper-connection mixer. Shapes include
**rank-3** tensors: the depthwise conv and the fused expert stacks, which carry
no `.weight` suffix. And the skip set (`model.visual.`, `mtp.`) is mandatory
rather than optional, because all 333 vision tensors share a shard with 16 text
tensors, so a loader reading whole shards must classify per tensor.

`read.rs` has no Gemma 4 counterpart: that line's checkpoint is small enough to
hold whole, while this one is 360 GB across 131 shards, so the reader reports the
shards it never opened and refuses to call their tensors missing, while still
holding every opened tensor to its shape and dtype. It also cross-checks each
shard's header against the names the index attributes to it, so a stale index is
named instead of surfacing as a wall of spurious "missing".

The contract derives exactly the frozen checkpoint's text set — 1658 keys less
333 `model.visual.*` and 31 `mtp.*` = 1294 tensors, `lm_head.weight` the only one
outside the text prefix — and has been held against eight real, sha256-verified
shards (446 tensors opened, 111 of them required text tensors): every shape and
dtype matched. The n-gram partition is confirmed **by value** against the two
`I64[16]` vectors the checkpoint stores, not only by shape.

CPU only, like the rest of the crate: 67 tests run with no device, no weights and
no Triton-equipped build, and three more read real shard bytes on demand.

Refs pegainfer-project#1104

Signed-off-by: bohaotu <bohaotu@pony.ai>
sysuTobo added a commit to sysuTobo/pegainfer that referenced this pull request Oct 12, 2026
An audit of this PR found two issues that matter and six worth tightening; all
eight sit in the gap between what the contract claims and what it makes
verifiable. None of them changes a derived number.

- The ignored gate printed how many tensors it opened but not how many of them
  the contract requires, so the "446 opened / 111 required" figure in the
  bring-up doc could not be reproduced from the gate's own output. It prints the
  required/skipped split now.
- The fault message named "Qwen3.8-Flash-Next" while this line's probe and config
  deliberately serve *any* smaller `qwen4_exp` checkpoint, so a failure on a
  second checkpoint would name the wrong model. It names the architecture
  (`MODEL_TYPE`) now.
- `dtype_size` existed twice and the two disagreed: the production copy returned
  `0` for an unknown dtype, silently shrinking `weight_bytes`, while the test copy
  panicked. There is one copy in `manifest/mod.rs` now, and it panics for both.
- `schema.rs` guarded duplicate derivations with `debug_assert!`, which the
  `--release` profile these tests run under compiles out. It is an `assert!` now.
- The three `dead_code` allows still said "the tensor contract is the first
  reader", which this PR makes stale; they name the actual first reader (slice B's
  loader, and the contract test for `bf16_bytes`).
- Two doc sentences over-claimed: "every probe, config and manifest test runs
  against it" (two of them build their own input) and "three that read real shard
  bytes" (one reads the index).

Not addressed, by decision: the line is not wired into CI (pegainfer-project#1130 kept it out until
slice B), so these tests still run by hand only. Recorded in the PR description as
a known bound rather than fixed here.

Signed-off-by: bohaotu <bohaotu@pony.ai>
FeathBow added a commit that referenced this pull request Oct 14, 2026
* feat(qwen4): enforce the tensor contract before any device work

The detection PR (#1130) landed the probe and the validated config, so the
checkpoint's tensors were implied by the geometry but never named. This lands the
contract that says exactly which tensors the line must read, and holds the
checkpoint's own headers against it before any H2D.

Ported from `pegainfer-gemma4/src/manifest/`, with three deltas this checkpoint
forces. The LM head is **untied**, so both ends are required and there is no
final-norm tensor — the readout is the hyper-connection mixer. Shapes include
**rank-3** tensors: the depthwise conv and the fused expert stacks, which carry
no `.weight` suffix. And the skip set (`model.visual.`, `mtp.`) is mandatory
rather than optional, because all 333 vision tensors share a shard with 16 text
tensors, so a loader reading whole shards must classify per tensor.

`read.rs` has no Gemma 4 counterpart: that line's checkpoint is small enough to
hold whole, while this one is 360 GB across 131 shards, so the reader reports the
shards it never opened and refuses to call their tensors missing, while still
holding every opened tensor to its shape and dtype. It also cross-checks each
shard's header against the names the index attributes to it, so a stale index is
named instead of surfacing as a wall of spurious "missing".

The contract derives exactly the frozen checkpoint's text set — 1658 keys less
333 `model.visual.*` and 31 `mtp.*` = 1294 tensors, `lm_head.weight` the only one
outside the text prefix — and has been held against eight real, sha256-verified
shards (446 tensors opened, 111 of them required text tensors): every shape and
dtype matched. The n-gram partition is confirmed **by value** against the two
`I64[16]` vectors the checkpoint stores, not only by shape.

CPU only, like the rest of the crate: 67 tests run with no device, no weights and
no Triton-equipped build, and three more read real shard bytes on demand.

Refs #1104

Signed-off-by: bohaotu <bohaotu@pony.ai>

* fix(qwen4): harden the tensor contract after its self-review

An audit of this PR found two issues that matter and six worth tightening; all
eight sit in the gap between what the contract claims and what it makes
verifiable. None of them changes a derived number.

- The ignored gate printed how many tensors it opened but not how many of them
  the contract requires, so the "446 opened / 111 required" figure in the
  bring-up doc could not be reproduced from the gate's own output. It prints the
  required/skipped split now.
- The fault message named "Qwen3.8-Flash-Next" while this line's probe and config
  deliberately serve *any* smaller `qwen4_exp` checkpoint, so a failure on a
  second checkpoint would name the wrong model. It names the architecture
  (`MODEL_TYPE`) now.
- `dtype_size` existed twice and the two disagreed: the production copy returned
  `0` for an unknown dtype, silently shrinking `weight_bytes`, while the test copy
  panicked. There is one copy in `manifest/mod.rs` now, and it panics for both.
- `schema.rs` guarded duplicate derivations with `debug_assert!`, which the
  `--release` profile these tests run under compiles out. It is an `assert!` now.
- The three `dead_code` allows still said "the tensor contract is the first
  reader", which this PR makes stale; they name the actual first reader (slice B's
  loader, and the contract test for `bf16_bytes`).
- Two doc sentences over-claimed: "every probe, config and manifest test runs
  against it" (two of them build their own input) and "three that read real shard
  bytes" (one reads the index).

Not addressed, by decision: the line is not wired into CI (#1130 kept it out until
slice B), so these tests still run by hand only. Recorded in the PR description as
a known bound rather than fixed here.

Signed-off-by: bohaotu <bohaotu@pony.ai>

* fix(qwen4): name an indexed shard that is not a regular file

The contract audit left one case uncovered: an indexed path that exists but is not
a regular file was counted as "not downloaded", so a download tool that left a
directory (or any other non-file) behind passed as an ordinary partial fetch. On a
360 GB checkpoint fetched shard by shard that is a plausible accident.

- The scan gains `unreadable_shards`, filled only when the indexed path exists and
  is not a file. `absent_shards` keeps its narrower meaning.
- `check` names it as its own fault class, and forgives `missing` for it as well:
  a shard we cannot open is one we cannot testify about, so its tensors must not be
  reported as a wall of "missing" either.
- The ignored real-shard gate counts it in its denominator, so "opened X of Y"
  stays honest.
- The two ignored tests that read the real checkpoint index now use
  `read::INDEX_FILE` instead of repeating the literal, so the published filename
  has one source.

New test: 68 passed, 3 ignored; clippy `-D warnings` and `fmt --check` clean.

Signed-off-by: bohaotu <bohaotu@pony.ai>

* docs(qwen4): refresh the compiled-GQA trap and trim the comments

Review feedback on the GQA trap was right: `SUPPORTED_GQA_GROUP_SIZES` is
`[1, 2, 3, 4, 6, 8]` on `main` now — #1145 routed group 6 — so Qwen3.5-27B is no
longer "in the same corner". Group 12 is still absent, so the conclusion holds; the
bullet now states the set and the mechanism (it grows when a group earns a compiled
kernel) instead of the stale comparison.

Also, per review:

- The verification section recorded the internal provenance of the run — which
  shards, how much was fetched, the checksum procedure, the tensor counts. That
  belongs in the local working notes, not the repository: it keeps the claim and the
  two derived conclusions, and the ignored gate still prints what it opened.
- The ignored n-gram test no longer names a specific shard of the frozen revision,
  and the comments across the module are cut to a line or two where they ran to
  paragraphs.

Comments and docs only: no derived number, name, shape, test or behaviour changed.

Signed-off-by: bohaotu <bohaotu@pony.ai>

* fix(qwen4): hold the contract against a snapshot of every shard header

Signed-off-by: Feathbow <feathbow@gmail.com>

* test(qwen4): drop the tests the header snapshot already covers

Signed-off-by: Feathbow <feathbow@gmail.com>

* docs(qwen4): describe the contract as tested and leave decode capture to slice B

Signed-off-by: Feathbow <feathbow@gmail.com>

---------

Signed-off-by: bohaotu <bohaotu@pony.ai>
Signed-off-by: Feathbow <feathbow@gmail.com>
Co-authored-by: Feathbow <feathbow@gmail.com>
@sysuTobo
sysuTobo deleted the feat/qwen38-flash-next-detection branch October 14, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants