Skip to content

Fuse the native while-loop body under mx::compile #163

Description

@ausimian

Context

Native Bumblebee.Text.generation (the CM7–CM13 expr-compiler work on feat/expr-compiler) compiles the whole generation graph — transformer forward + defn while decode loop + KV cache + sampling — to the single-NIF replay. Measured ~5.07× over the evaluator on Qwen3-0.6B greedy decode (~61 vs ~12 tok/s, byte-identical output; see bench/qwen3_tokens_per_sec.exs).

But that number is achieved with mx::compile fusion disabled. A while-containing program is a data-dependent, host-controlled loop that mx::compile can't trace, so eval_program degrades it to a plain sync replay (the guard in c_src/program.cpp's eval_program_nif). The elementwise runs the replay leaves separate (RMSNorm/softmax/SiLU gating/residual adds) are therefore unfused. CM6 measured ~1.5–1.6× from this fusion on a decode-shaped transformer block, so there's likely meaningful headroom on top of the 5×.

Idea

The outer loop can't be traced, but the loop body (the per-token forward sub-program, instr.subprograms[1] of the While instruction) is shape-stable — offset is a runtime s32 input, so one compiled program serves every position. Wrap just the body sub-program in mx::compile (cached per stream, like the existing CM6 Program::CompiledEntry machinery), replaying the fused callable each iteration. The condition sub-program could get the same treatment.

Where

  • c_src/program.cpp — the While arm in replay_program: instead of replay_program(body_p, state, s) raw, replay a per-stream-cached mx::compile'd version of the body.
  • Reuse the Program::compiled per-stream cache pattern (program.hpp) — but keyed/stored on the child (body) program.

Caveats / acceptance

  • Not bit-identical. mx::compile reassociates f32 (last-few-ULP), so the fused decode won't match the evaluator bit-for-bit (same caveat as CM6). generation_native_test.exs asserts bit-identical, so this must be opt-in (e.g. an eval-mode/option flag) or its conformance gate must switch to an all-close tolerance + a greedy-token-prefix match rather than exact.
  • Shape-stability holds (offset is a runtime input), so the compiled body should cache-hit across tokens — verify it does, not recompile per step.
  • Acceptance: bench/qwen3_tokens_per_sec.exs shows the fused-while native lane beats the current native lane on Qwen3-0.6B; document the speedup and the tolerance trade-off.

Follow-up to the CM13 native-generation work (PRs #156#162).

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