Skip to content

Geometry-general native Qwen3.5 dense loader #503

Open

Description

@akol1

Issue: geometry-general native Qwen3.5 dense loader (Metal side)

Backend-scale proposal per CONTRIBUTING ("a new backend ... open an issue first"): the
native Metal engine serves exactly one geometry today. Stacks on #490: that draft
already generalizes the shared family code — a geometry-level config.zig for "every
backend" plus a weights/ module tree — while explicitly keeping "Metal's admission of
the tied-head 2B MLX affine checkpoint". This issue is the Metal-side completion of that
split.

What is pinned, from source (main @ 1.0.2)

Pin Where
Geometry constants zig/src/families/qwen3_5/config.zig (hidden = 2048, layers = 24, intermediate = 6144, ...); parse refuses any other integer (error.UnsupportedQwenGeometry)
Format config.zig quantization check refuses anything but 4-bit / group 64
Head landed as configurable: the untied-head companion PR reads language_model.lm_head and cross-checks tiedness
Kernels zig/kernels/metal/qwen3_5/kernels.zig lists one captured .metal source per projection shape (qmm_6144_2048, qmm_2048_2048, ...); Kernels.projection(n, k) is a comptime lookup, so a checkpoint with a shape outside the table cannot dispatch
Dispatch forward.zig derives grids from the comptime constants (c.intermediate, c.conv_dim, head counts)

The captured qmm kernels are MLX-derived, shape-baked Metal sources produced by the capture
tooling (tools/zig/capture_qwen35.py + the op-fixture harness). Recompiling MLX for a new
geometry is not a build step the repo has.

Why it matters

Qwen3.5-hybrid dense checkpoints now ship at several geometries (a 9B-class model is
24 layers -> 32, hidden 2048 -> 4096, untied, intermediate 12288). The Python line loads
them through mlx-lm with row kernels; the native engine — the faster path on every tested
Mac — refuses them at load. Untied heads are already unblocked by the companion PR; the
remaining pins are geometry and the per-shape kernel table.

Options, in ascending scope

Option A: qualified geometry set (smallest honest step)

Extend config.zig from one pinned geometry to an explicit table of admitted geometries;
generate and ship the captured kernel set for each (the capture tooling already exists).
Admission stays strict; kernels stay captured; nothing at runtime is new.

  • Cost: a Mac session per geometry to capture and qualify (tf-qwen35-check,
    tf-qwen35-forward, tf-qwen35-exact).
  • Ships the 9B class if its shapes are admitted; every future geometry still needs a
    capture session.
  • No change to forward.zig except reading geometry from Config instead of comptime
    constants — a mechanical, reviewable diff.

Option B: runtime-shaped kernel compilation

Replace the per-shape captured qmm sources with one shape-parameterized Metal source per
kernel family, compiled at load with the checkpoint's dimensions
(mtl.Library.fromSource is already the load path; the release probes runtime-compiled vs
prebuilt libraries). The non-qmm kernels (norm, gdn_, attn_, mlp_act) already take their
dynamic extents from buffers/setValue — only the qmm projections bake shapes.

  • Cost: a new Metal kernel family whose arithmetic must match the captured MLX kernels bit
    for bit on the qualified path, or a documented deviation with fidelity receipts.
  • Risk: the captured kernels are MLX's, and matching them exactly is the load check's
    contract (weights.zig real-weight row checks, qualify.zig windows).

Option C: a generic affine row decoder (what the Python line already does)

Port the packed-row affine decoder concept (simd_qmm, 2-8 bit, groups 32/64/128) into a
Metal kernel family that dispatches on runtime (n, k, bits, group). This also unlocks
mixed-width checkpoints natively, which the current 4/64-only admission refuses.

  • Cost: the largest; a real kernel project with exactness gates per CONTRIBUTING.
  • Payoff beyond geometry: mixed affine checkpoints (3/4/6-bit recipes) serve natively at
    full speed instead of falling back.

Recommendation

A then C: land the geometry-table refactor (no arithmetic change, immediately testable)
and qualify the 9B shapes via capture; treat C as the follow-up that retires the capture
loop. B alone is strictly worse than C for this purpose.

Testing plan

  • Config admission tests extend to the table (Linux-runnable today).
  • Every new geometry: capture fixtures on the target GPU, then tf-qwen35-check,
    tf-qwen35-forward, tf-qwen35-exact, plus tools/bench_concurrent.py,
    tools/bench_openai.py, tools/prefill_cold.py receipts per CONTRIBUTING.
  • The untied-head PR's cross-check keeps admission honest per geometry.

Acceptance

A 9B-class untied checkpoint serves natively on an M-series Mac with drafted-vs-plain and
concurrent-vs-solo equality, and the recipe table grows a per-geometry qualification row.

Activity

  1. changed the title [-]geometry-general native Qwen3.5 dense loader[/-] [+]Geometry-general native Qwen3.5 dense loader[/+] on Oct 8, 2026
  2. ashhart commented on Oct 10, 2026

    @ashhart
    Owner

    Thank you, @akol1. Two parts of this are settled. #493 landed: the native 2B serves untied heads, and a qwen3_5 checkpoint opens its engine by geometry. The 27B stays on its own engine, with one path per model, so #520's second route for it was closed. Admitting another dense geometry, such as the 9B, still needs a qualification run on Metal for that checkpoint, so this issue stays open until one is scheduled.

  3. ashhart commented on Oct 10, 2026

    @ashhart
    Owner

    An update, @akol1: we're going to qualify the 9B now, starting with mlx-community/Qwen3.5-9B-MLX-4bit (938d8919: hidden size 4096, 32 layers, an untied head, MLX affine 4-bit). On main a qwen3_5 checkpoint that isn't the pinned 2B opens the 27B's engine, which admits only the 27B's shape, so the work is taking that shape check out in favour of the geometry from the config, then the same gate the 27B passed on Metal: drafted equal to "draft": false, resumed equal to fresh, concurrent equal to solo, and prefill and decode against mlx_lm on the same Mac. We'll post the results here.

  4. ashhart commented on Oct 21, 2026

    @ashhart
    Owner

    Thank you for the report. The 9B Metal path still needs pinned whole-model qualification, including reference agreement and draft, resume and concurrency checks. We are keeping this open rather than treating general geometry work as proof that this checkpoint is qualified.

    - Synthema, Ash's AI Assistant.

  5. ashhart commented on Oct 27, 2026

    @ashhart
    Owner

    Sorry for the silence here, @akol1. This is still open on main: a qwen3_5 checkpoint that isn't the 2B opens the 27B's Metal engine, which still admits only the 27B's shape (zig/src/families/qwen27/model.zig:162), so the 9B is refused. The next step is ours: take that shape from the checkpoint's config and add the 9B's projection shapes, then run the 9B's gate on an M5 (drafted equals plain, resumed equals fresh, concurrent equals solo, and prefill and decode against mlx_lm). We'll post the results here.

  6. ashhart commented on Oct 28, 2026

    @ashhart
    Owner

    /atc claim

  7. grantoverton commented on Nov 1, 2026

    @grantoverton
    Contributor

    A qualification run for the 9B is up as a PR family, in case it helps here:

    Next in the family: more 9B kernels (M5 K slices, 5-bit byte codes, M3 rows). Happy to fold any of it into your branch or reshape it to fit what you have planned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions