Skip to content

quant, card leg: Qwen3.5/3.6's placed card leg runs the common leg's Q8_0, Q6_K, IQ3_XXS and IQ4_XS experts (#75) - #629

Merged
midagedev merged 10 commits into
mainfrom
q75
Oct 22, 2026
Merged

midagedev merged 10 commits into
mainfrom
q75

Conversation

@midagedev

@midagedev midagedev commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Part of #75. Qwen3.5/3.6-35B-A3B files with Q8_0, Q6_K, IQ3_XXS or IQ4_XS experts kept every routed expert on the host. The placed load now runs those layers on the common card leg, so a 24 GB card holds 44–57 % of the experts at the default context.

What changes:

  • accepts.rs: Body35's placed card leg takes the common leg's routed rows (gate·up Q6_K, IQ3_XXS, IQ4_XS, Q8_0; down IQ3_XXS, IQ4_XS, Q5_1, Q8_0, IQ4_NL). The common leg's prompt rows have their own names (prompt_leg_gate_up, prompt_leg_down, LegSplit).
  • The card leg gains a Q6_K gate·up kernel (q6k_gate_up_sel) and the Q8_0 and Q6_K gate·up arms.
  • Body35's placed load runs a layer its dispatch refuses on the common card leg. The dispatch's own layers stay bit-identical.
  • Types the leg still cannot run stay named refusals, with the reason in the doc comment (IQ2_XS, IQ3_S and MXFP4 have no prompt GEMM).

Plan on the RTX 3090, bloomery-serve --model qwen3 --plan, --place unset [measured]:

file ctx card experts / 10240 before
Q8_0 262144 4505 0
Q8_0 16384 6035 0
UD-Q6_K 262144 5844 0
UD-Q6_K 16384 7685 0
Q4_K_M (control) 262144 8385 8385

Predicted decode [derived from that plan, uniform routing, not timed]: Q8_0 ×1.4 at ctx 262144 and ×1.6 at 16384; UD-Q6_K ×1.5 and ×1.7. The timed A/B comes later on this PR.

Evidence:

  • New gate gate-gpu-qwen35moe-leg: the common-placed layers against the host path on the base fixture and four requantized variants (q35iq4xs, q35q80, q35iq4q8, q35q6k). The wide arm's error is 1.9e-2 to 2.3e-2 against the band 1e-1. The prompt call is bit-identical to the graph step on every load.
  • FAIL-first: five mutants (two tables, the Q8_0 arm, a gate/up swap in the route block, and the Q6_K kernel reading up from the gate weights). Each turns its clause red.
  • just affected origin/main on the fixture tier: 229 items, red 1 (qwen35moe-e2e unjoined variant: the card had 649 MB free from a co-tenant; rerun alone).
  • Real tier: gate-placement, gate-gpu-gates-lib, gate-gpu-qwen3moe-e2e, gate-gpu-qwen4exp-iqleg, gate-gpu-qwen3moe-down, gate-gpu-qwen38-residency, gate-gpu-qwen35moe-e2e green (7, red 0).
  • Rebased on main after gpu: make the partial head-256 cache append unsafe #612, which touches the same qwen3moe files: the 39 gates that reach them are green on the fixture tier.
  • Ratchets: unsafe gpu 2112 → 2115 for q6k_gate_up_sel (on top of gpu: make the partial head-256 cache append unsafe #612's 2101 → 2112); model-tax accepts.rs qwen 26 → 30 (Body35's own program rows), coverage.rs 63 → 61. Each carries a PIN line.

Review fix: LegStacks::with_arms is folded into of_arms, so one constructor remains (beadc2e, host only).

Cards: the maintainer's box, RTX 3090 and RTX A6000. The gates ran at 12f6953 on main 3938149, and at ade2c45 on main 1e8e923 for the rebase. The fixture tier reads the qwen35moe fixture and its requantized variants under /models. The timed decode and prompt A/B follows on this PR.

This work was written with AI help: a Claude subagent wrote the change, and Claude reviewed it and ran the gates.

🤖 Generated with Claude Code

@midagedev midagedev added enhancement New feature or request gpu The card engine: crates/gpu*, crates/app placement Placement planning: crates/placement needs-gpu Changes code a card must check, and carries no gate run on a card labels Oct 21, 2026
@github-actions github-actions Bot added model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo labels Oct 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: MUST_FIX: Remove the redundant constructor layer.

Must fix

1. Keep one constructor for explicit leg arms

crates/gpu/src/card_leg.rs:594 Kind B, fewest elements: of_arms forwards all four arguments unchanged to with_arms. The two functions implement one operation with no separate contract.
Collapsing them now removes a name before more callers adopt it. Replace both definitions with this definition, keep the public constructor's documentation, and change the three internal LegStacks::with_arms calls to LegStacks::of_arms.

pub fn of_arms(
    w: &'a Weights,
    names: [&'a str; 3],
    arms: LegArms,
    roles: (&'static str, &'static str),
) -> Result<LegStacks<'a>, GpuError> {
    let [gate, up, down] = names;
    let (gate, up, down) = (
        LegStack::of(w, gate, arms.gate_up, kernels::card_planes_runs, roles.0)?,
        LegStack::of(w, up, arms.gate_up, kernels::card_planes_runs, roles.0)?,
        LegStack::of(w, down, arms.down, kernels::card_planes_runs, roles.1)?,
    );
    if up.ty != gate.ty {
        return Err(GpuError::Tensor {
            what: WHAT,
            name: up.name.to_owned(),
            need: "the gate stack's type",
        });
    }
    Ok(LegStacks { gate, up, down })
}

Non-blocking

⚠ Supply decode and prefill timing evidence: crates/gpu/src/arch/qwen3moe/placed.rs:1633.
Each newly carded common layer adds four expert-path launches per decode token: 4 × newly carded common layers (input quantization, gate/up, hidden quantization, down). Wide prompts move host arithmetic to routed GEMMs.
The float sum order/precision class requires the owning gate and a timed A/B. Attach same-lease decode and prefill results on the contributor's card; the PR explicitly postpones timing.

Checked

  • Duplicates/elements: read both constructor definitions and all call sites. Collapsing them removes one function. Dryer reports zero pairs and excludes gpu/gpu-gates.
  • Complexity: CardLeg::launch and shadow add quant and leg dispatch branches, consistent with the diagnostics' reported growth.
  • Silent failure: the constructor propagates stack errors and rejects mismatched gate/up types; the proposed replacement preserves both checks.
  • Tests: inspected the Q8_0/Q6_K hand-launch comparison and the placed-load gate. Runtime gates were not rerun.
  • Gates: the raised unsafe and model-tax limits have dated PIN reasons.
  • Escape catalog: C1–C4 do not match the cited constructor or dispatch changes.

Notes

Static validation only. The PR body lacks the nvidia-smi line, both tested commit hashes, exact model paths and gate output required for card evidence. Its fixture summary reports one red item.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen35moe_e2e.rs, crates/gpu-gates/src/record.rs, crates/gpu/src/arch/qwen3moe/body35.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@midagedev

Copy link
Copy Markdown
Owner Author

Decode A/B, RTX 3090, one lease. depth-serve.sh ran with one fresh server per arm and round, 4 rounds, D = 6, N = 128, ctx 262144 and --place unset. The base arm is main 1e8e923 and the changed arm is ade2c45 + the card. Card: docs/cards/q75-decode.card (predicted +25..+60 %).

file main tg tok/s this PR tg tok/s change
Qwen3.6-35B-A3B-Q8_0 68.73 89.60 +30.4 % ± 3.6 %
Qwen3.6-35B-A3B-UD-Q6_K 81.59 104.96 +28.6 % ± 13.9 %

Both are inside the band, but below the derived ×1.4 and ×1.5. Two of the four Q8_0 rows carry [cpu-busy] because a gate batch was building on the box. Prompt rates at D = 6 are not meaningful here.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: MUST_FIX: two validation checks repeat existing owners, and the new route block carries an unused activation choice.

Must fix

1. Share the routed-stack shape check

crates/gpu/src/arch/qwen3moe/placed.rs:1235 repeats crates/gpu/src/arch/qwen3moe/body.rs:283: resident KQuant form, gate/up/down dimensions and expert-row counts (R14).
These memory-layout checks encode the same fact. Reusing the existing validator removes the repeated layout logic (kind B).
Make stack_files pub(super), then replace body::stack_tys with:

pub(super) fn stack_tys(
    w: &Weights,
    names: [&str; 3],
    shape: (usize, usize, usize),
    l: usize,
) -> Result<[SiteTy; 3], GpuError> {
    let raw = super::placed::stack_files(w, names, shape, l)?;
    let site = |i: usize| {
        SiteTy::of_ggml(raw[i]).ok_or_else(|| {
            GpuError::shape(
                "qwen3moe::Body::load",
                format!("{} is {}; no launch family reads it", names[i], raw[i]),
            )
        })
    };
    let tys = [site(0)?, site(1)?, site(2)?];
    stack_entries(tys, l)?;
    Ok(tys)
}

2. Reuse the existing card-eligibility predicate

crates/placement/src/accepts.rs:212 repeats the gate/up equality and role checks owned by crates/placement/src/accepts.rs:394 (R14).
One owner avoids separate eligibility edits in the load and residency paths (kind B). Replace the classifier with:

pub fn qwen35_leg([gate, up, down]: [GgmlType; 3]) -> Option<LegSplit> {
    if leg_runs(Program::Qwen35Body, [gate, up, down]) != Some(true) {
        return None;
    }
    Some(if qwen35_gate_up(gate).is_some() && qwen35_down(down).is_some() {
        LegSplit::Dispatch
    } else {
        LegSplit::Common
    })
}

3. Remove the unused activation-rule choice

crates/gpu/src/card_leg.rs:296 adds rule and alternative-rule dispatch. Its only constructor, crates/gpu/src/arch/qwen3moe/placed.rs:1685, always supplies Act::SiluMul. No second caller is added.
The fewest-elements contract makes this unused field and branch a kind B must-fix.
Delete RouteBlock::rule and its initializer. Replace the grouped SwiGLU call with:

gemm.enqueue_swiglu_quant(stream, g, u, n_slots, act, fault)?;

Replace the entire match self.rule in the Blocks32 arm with:

g32.enqueue_swiglu_quant32(stream, g, u, n_slots, act, fault)?;

Non-blocking

⚠ Timed proof is pending: crates/gpu/src/arch/qwen3moe/placed.rs:1633 runs four card-leg launches per common layer/token and changes host arithmetic to card q8_1 arithmetic. The float-precision change class requires its gate and timed A/B. Attach same-lease decode and prefill results on the contributor's card; the body defers them.

Optimization headroom

Checked

Duplicates: both cited validation copies confirmed. Dryer reported zero pairs and excluded GPU files.
Elements: the new card_leg::RouteBlock has one constructor, always using Act::SiluMul; its replacement launchers exist.
Performance: traced route's q8_1 output and the common leg's four launches, including the repeated input quantization.
Inline: replacement verified against the added line and existing leg_runs implementation.
Tests: hardware tests not run. The PR body reports gates and FAIL-first mutants.
Escape catalog: no matching patterns.

Notes

The evidence lacks the nvidia-smi card/driver line and gate commands with output.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen35moe_e2e.rs, crates/gpu-gates/src/record.rs, crates/gpu/src/arch/qwen3moe/body35.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

Comment thread crates/placement/src/accepts.rs Outdated
/// residency stack table and the placed side read.
#[must_use]
pub fn qwen35_leg([gate, up, down]: [GgmlType; 3]) -> Option<LegSplit> {
if gate != up || !prompt_leg_gate_up(gate) || !prompt_leg_down(down) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard repeats leg_runs; reuse the existing card-eligibility predicate.

Suggested change
if gate != up || !prompt_leg_gate_up(gate) || !prompt_leg_down(down) {
if leg_runs(Program::Qwen35Body, [gate, up, down]) != Some(true) {

@midagedev

Copy link
Copy Markdown
Owner Author

The three must-fix items are in dc5e512: body::stack_tys reads its stacks through placed::stack_files, qwen35_leg reads leg_runs(Program::Qwen35Body, …), and RouteBlock drops its rule field (every caller ran SiluMul).

Proof: just narrow f5bf581d2 dc5e512cd — the --no-jit scans of the covering set (bloomery-serve, gate_kquant, gate_swap) are identical, so no kernel moved. The tool kept its full list only because crates/gpu/src/card_leg.rs has no gate-paths row. I ran the gates on the changed host paths (card_leg's RouteBlock is used only by placed.rs's wide arm):

  • fixture: gate-gpu-qwen35moe-leg and its 4 variants (q35iq4xs, q35q80, q35iq4q8, q35q6k), gate-gpu-qwen35moe-e2e, gate-gpu-qwen3moe-types, gate-gpu-lib alone — all green
  • real: gate-placement, gate-gpu-qwen3moe-e2e — green

github-actions[bot]
github-actions Bot previously approved these changes Oct 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the last review:

  • Fixed: routed-stack shape checks now share stack_files.
  • Fixed: card eligibility now reuses leg_runs.
  • Fixed: RouteBlock no longer carries an unused activation choice.
  • The earlier timed-proof question concerns code unchanged by this increment and is excluded under the incremental-review scope rule.

Verdict: CLEAN: the three blocking findings are fixed, and no must-fix finding remains in this increment.

Checked

Duplicates: no new definitions; both validation copies now reuse existing owners.
Complexity: RouteBlock::enqueue loses the activation-choice match. Its caller previously supplied Act::SiluMul; the selected launchers preserve that rule.
Silent failure: four paths followed: missing tensor, wrong shape, unsupported type and unequal gate/up. They return named errors or refuse card placement.
Tests: no tests changed. Source inspection shows that dropping gate/up equality from leg_runs would fail the existing classifier assertion at accepts.rs:594; the mutant was not run. Builds and hardware gates were not run.
Gates: no pins, thresholds or test cases changed in this increment.
Escape catalog: no C1–C4 pattern matched.

Notes

The PR body lacks the card/driver nvidia-smi line and gate commands with their output required for complete GPU evidence.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen35moe_e2e.rs, crates/gpu-gates/src/record.rs, crates/gpu/src/arch/qwen3moe/body.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

midagedev and others added 10 commits October 22, 2026 09:00
…(Q6_K, IQ3_XXS, IQ4_XS and Q8_0 gate·up; IQ3_XXS, IQ4_XS, Q5_1, Q8_0 and IQ4_NL down); the coverage pins and the host-only causes follow the table (#75)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_K gate·up arms; Body35's placed layers launch them (the common leg's plan rule still admits neither)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he common card leg; the dispatch's layers stay bit-identical

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…four fixture variants (q35iq4xs, q35q80, q35iq4q8, q35q6k)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate_up, prompt_leg_down, LegSplit); only qwen35_leg names the family

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s Qwen35Body's own program rows, coverage.rs's comes down

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arms (with_arms folded in)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aced card leg (RTX 3090, ctx 262144)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
body::stack_tys reads its stacks through placed::stack_files, qwen35_leg
reads leg_runs(Program::Qwen35Body, ...), and RouteBlock drops its rule
field: every caller ran SiluMul.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the last review:

  • Still fixed: routed-stack validation shares stack_files.
  • Still fixed: card eligibility reuses leg_runs.
  • Still fixed: RouteBlock carries no unused activation choice.

Verdict: CLEAN: the model-tax increase matches the source count and has a dated reason.

Checked

Duplicates: 0 new items; dryer reports 0 pairs. This increment changes one table row.
Complexity: no functions changed. The crapper results concern earlier commits.
Silent failure: 0 new sites; no runtime behavior changed.
Tests: no tests changed. Independent counting confirms 31 production lines matching the qwen token; the old allowance of 30 rejects the source. One extra matching line exceeds the new allowance. Full checks and hardware gates were not run.
Gates: tools/model-tax.tsv:169 rises from 30 to 31. Its PIN(2026-10-22): explains the shared-classifier call at accepts.rs:212; the count confirms the reason. The two-sided check still rejects both excess and unused allowance.
Escape catalog: no C1–C4 pattern matched.

Notes

GPU evidence remains incomplete: the supplied material lacks the card/driver nvidia-smi line and gate commands with their output required by CONTRIBUTING.md.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen35moe_e2e.rs, crates/gpu-gates/src/record.rs, crates/gpu/src/arch/qwen3moe/body.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@midagedev
midagedev merged commit 0fd33f0 into main Oct 22, 2026
2 checks passed
midagedev added a commit that referenced this pull request Oct 22, 2026
#627's Q5_0 GEMM entry and #629's exhaustive match landed apart, so
main did not build; Q5_0 runs the 32-value GEMM (Q5_0Packed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gpu The card engine: crates/gpu*, crates/app model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja needs-gpu Changes code a card must check, and carries no gate run on a card placement Placement planning: crates/placement tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant