Skip to content

refactor(ops): migrate core/elementwise primitives into kernels/ops/ (completes wave 1) - #289

Closed
ekryski wants to merge 17 commits into
0xClandestine:devfrom
thewafflehaus:ek/kernel-reorg-ops
Closed

refactor(ops): migrate core/elementwise primitives into kernels/ops/ (completes wave 1)#289
ekryski wants to merge 17 commits into
0xClandestine:devfrom
thewafflehaus:ek/kernel-reorg-ops

Conversation

@ekryski

@ekryski ekryski commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

What

Final wave-1 kernel-family port: moves the core / elementwise primitive
kernels out of the legacy mlx/ + ffai/ split into kernels/ops/ per
docs/specs/KERNEL_CONSOLIDATION_PLAN.md. With this, wave 1 is complete
(rope · norm · sampling · ops).

Changes

  • mlx/{arange,arg_reduce,binary,binary_two,copy,gather_axis,hadamard,hadamard_m, indexing,logsumexp,random,reduce,scan,scatter_axis,strided,ternary,unary}
    kernels/ops/ (already mt_*; these are inventory-registered, so there were
    no path-import sites to fix beyond one snapshot test).
  • ffai/{axpy_scalar_inplace,clamp_scalar,gather} + mt_vector_add.rs (→
    vector_add.rs) → kernels/ops/ with ffai_mt_ prefixes.
  • Dedup: deleted ffai/arg_reduce.rsffai_argmax was a byte-for-byte
    duplicate of mt_argmax (verified line-by-line), so it's dropped, not moved.
  • fence.rs moves but stays intentionally undeclared — device/system memory
    fences and atomics have no #[kernel] DSL representation (the "1 out of scope"
    op in the audit); the file is kept for its implementation notes.
  • Updated the steel_msl_snapshots import, the op_group / name-filter unit-test
    fixtures, the plan (ops done, wave 1 complete), and the KERNEL_AUDIT rows.

Generated MSL per kernel is unchanged — only module paths and inventory names.

Verification

  • cargo build -p metaltile-std + cargo clippy --workspace --all-targets clean.
  • All ops kernel tests pass via tile test (binary, unary, ternary/select, copy,
    strided, arange, random, reduce, argmax/argmin, scan, indexing, gather(_axis),
    scatter_axis, hadamard(_m), logsumexp, clamp, axpy, vector_add).
  • steel_msl_snapshots integration test passes.

Note on stacking

Stacked on sampling (#288) → norm (#287) → conv+rope (#286) → docs (#284); the
diff includes those until they merge. Each family lands as its own PR.

ekryski added 17 commits June 12, 2026 05:59
These two planning docs under crates/metaltile-std/src/convolution/ are
duplicates of the canonical specs in docs/specs/:

  convolution/PATTERNS.md  ->  docs/specs/CONV_KERNEL_PATTERNS.md
  convolution/PLAN.md      ->  docs/specs/CONV_CONSOLIDATION_PLAN.md

Keep the docs/specs/ copies as the single source of truth; nothing references
the in-tree duplicates.
The doc described the pre-#275 single-backend, in-process-runner design. Bring
it current:

- Multi-backend codegen: the CodegenBackend seam (Target = Metal/Cuda/Hip/Spirv),
  TargetProfile (lane width, MmaStrategy), and the msl/cuda/hip/spirv generators.
  Metal is the default/mature backend; CUDA/HIP/Vulkan device execution is
  feature-gated and validated by the GPU-vs-GPU reference corpora.
- Subprocess execution: the runner is no longer in-process — `tile` spawns
  `__tile_runner` (project-linked, inventory populated) and streams ProtocolMessage
  JSON lines. Rewrote the "subprocess readiness" section accordingly.
- metaltile-std modules: mlx / ffai / convolution / quant (was mlx + ffai only).
- Commands: added clean + config; clarified `emit` is `build --emit
  msl|metallib|swift|ir|all` and that --backend applies to bench/test only.
- Fixed companion-doc links (../cli.md, ../developing.md) and the now-renamed
  metric path (to_gflops / estimate_profile / classify_bottleneck /
  device_specs::lookup → ProfileInfo; was derive_metrics / DerivedMetrics).
Three overlapping docs (KERNEL_ORGANIZATION, CONV_CONSOLIDATION_PLAN,
CONV_KERNEL_PATTERNS) collapse into one singular reference,
docs/specs/KERNEL_CONSOLIDATION_PLAN.md — the kernels/<family>/ target layout
(no more mlx/ + ffai/), the three LOC-reduction tools (shared primitives,
#[kernel(variants)], op×format merge), the proven conv/ exemplar, and the
family-by-family migration order.

STYLE_GUIDE.md moves crates/metaltile-std/ → docs/ and becomes the authoring
authority ("how we want kernels written post-consolidation"): de-mlx/ffai'd,
the removed #[bench] name key fixed, a shared-primitives section added, family
mod.rs paths. developing.md's kernel-writing-philosophy now links to it; the
authoring patterns from CONV_KERNEL_PATTERNS fold in here rather than the plan.

Also fixes the stale ../specs/ links in docs/README.md (specs live at
docs/specs/), repoints ARCHITECTURE.md, and indexes both new docs.

Roadmap (where files go) vs authoring (how to write one kernel) are now one doc
each, with the redundancy across the old four removed.
The crate READMEs had drifted across several big changes. Fixes:

- metaltile-codegen / -runtime / metaltile (facade): no longer "Metal only".
  Codegen is multi-backend (CodegenBackend, Target = Metal/Cuda/Hip/Spirv,
  TargetProfile, the cuda/hip/spirv generators); runtime owns device/ (Metal +
  feature-gated cuda/hip/vulkan), dispatch/, cache/; the facade hosts harness/ +
  runner/ and runs GPU work in the __tile_runner subprocess.
- metaltile-core: GpuFamily lives in src/protocol.rs (not src/gpu_family.rs);
  documents the protocol wire types.
- metaltile-macros: corrected module paths (kernel/body.rs, kernel/sig.rs,
  bench.rs, derive/mod.rs), added kernel/variants.rs, and replaced the dead
  #[kernel(bench(op=,class=,mlx=))] system with the current #[bench] /
  #[test_kernel] / #[kernel(variants(...))].
- metaltile-std: rewritten — it's the kernel stdlib (mlx/ ffai/ convolution/
  quant/ probe/ utils), not "benchmark metadata"; dropped the removed
  BenchSpec/bench_types/run_spec framing.
- metaltile-cli: added the missing subcommands (test, clean, config, init,
  update, completions) and the --backend flag.

Cross-doc links point at the relocated style guide / consolidation plan.
…uestions

- BENCH_METRICS_SPEC.md: mark the metrics work + Appendix B precision support as
  implemented/complete; note M5 fp8/fp4 acceleration arrives with Metal 4.1;
  trim the stale capture context.
- KERNEL_CONSOLIDATION_PLAN.md: turn §9 open questions into decisions —
  (1) rename ffai_/model-prefixed kernels to mt_<op> (the FFAI emit consumer is
  regenerated, so names are not pinned); (2) keep vision/ + audio/ folders;
  (3) f16-scale twins fold into a ScaleKind format axis, not separate kernels;
  (4) kernels that mirror an upstream reference keep their .with_reference()
  comparator indefinitely. Reconciled §6 (rename step), §7 (scale axis), and §8
  (inventory names do change; emit is regenerated).
First family moved into the new kernels/<family>/ umbrella from the legacy
mlx/ + ffai/ split (the consolidation target — see KERNEL_CONSOLIDATION_PLAN.md).
git mv of all 7 rope files:

  mlx/rope.rs                  -> kernels/rope/base.rs   (mt_rope)
  ffai/rope_2d/_llama/_llama_many/_yarn,
  ffai/dsv4_partial_rope[_rows] -> kernels/rope/

Adds src/kernels/mod.rs + kernels/rope/mod.rs, wires lib.rs (pub mod kernels;),
removes the 7 decls from mlx/mod.rs + ffai/mod.rs, and repoints the two
intra-family/external references (rope_llama_many's test helper import and the
dsv4_partial_rope_rows correctness test). base.rs avoids the rope::rope
module-inception lint.

Pure structural move — pub fn names are unchanged (mt_rope, ffai_rope_2d, …), so
the kernel inventory and FFAI emit are byte-identical. Build + workspace clippy
clean; kernel_tests_harness green. LOC-reducing merges (rope_llama + _many,
partial_rope folding, ffai_/dsv4_ prefix drop) are the documented next phase.
…volution/

The #268 conv consolidation moved every 2D/3D/depthwise/winograd kernel into the
new convolution/ module (declared in lib.rs, fully built/tested/benched/emitted)
but left byte-identical copies behind in ffai/, un-declared in ffai/mod.rs —
13 orphaned files that compiled nowhere and duplicated the live kernels:

  conv2d{,_block_scaled,_mma,_mma_block_scaled}, conv3d{ -"- },
  depthwise_conv2d{,_block_scaled,_nhwc}, winograd_conv, kokoro

10 are bit-identical to their convolution/ twin; conv2d/conv3d_block_scaled
differ only by a one-line comment (the int8_decode rename). kokoro's two kernels
(lstm_cell, adain1d) are likewise superseded by the dedicated ffai/lstm.rs and
ffai/adain1d.rs modules. Nothing imports any of them.

Delete all 13, and fix the now-stale `ffai::conv2d` doc-ref in convolution/
conv3d.rs to `super::conv2d`. No kernel lost; the live ones stay in convolution/.
Harness green (0 failures), workspace clippy clean.
conv2d_mma_block_scaled was the lone block-scaled conv file still spelling out
all 29 per-format benches as explicit fns (~384 lines), while every sibling —
conv2d/conv3d/depthwise block_scaled and conv3d_mma — generates them from a
`*_bench_fmt!` macro. Replace the 29 fns with a `conv2d_mma_bench_fmt!` macro
(mirroring conv3d_mma's) + 29 one-line invocations, and adopt the siblings'
short bench names (bench_mxfp4, …) for naming consistency.

Behaviour-preserving: same kernels, same representative MMA shape
(1×64×32×32 → out_ch 1024, 1×1), same 29 formats incl. fp8_e4m3_f16 reusing
the nvfp8_f16 kernel. Build clean; kernel_registry_consistency green (all
benches codegen, no undefined symbols). −384 lines.
The four block-scaled conv files each spelled out 30 per-format correctness
tests as explicit fns (~600 lines/file), all following the identical pattern of
calling a shared setup helper with only (kernel, QFormat) differing — while the
sibling benches already collapse to a `*_bench_fmt!` macro. Mirror that on the
test side with a `*_test_fmt!` macro per file (conv2d/conv3d/depthwise +
conv3d_mma), replacing the explicit fns with one macro + 30 one-line invocations.

Behaviour-preserving: same 120 tests, same names, same uniform shape/tol per
file, same kernels (incl. fp8_e4m3[_f16] reusing the nvfp8[_f16] kernel). Each
file's kernel_ir_for count verified at 30. Full kernel_tests_harness green;
workspace clippy clean. ~2100 lines removed with zero coverage change.

This is option (a) of the test consolidation — a macro_rules! mirror of the
benches; migrating both to #[test_kernel(variants(...))] is deferred to the
broader kernel cleanup (see CONV_CONSOLIDATION_PLAN.md Phase 2).
…plan

Relocates the convolution family under the kernels/<family>/ layout
(docs/specs/KERNEL_CONSOLIDATION_PLAN.md). Pure git-mv — kernel pub fn
names are preserved so the inventory and FFAI emit are unaffected.

- crate::convolution -> crate::kernels::conv (lib.rs, mlx/steel re-export)
- drop the stale duplicate PATTERNS.md / PLAN.md (folded into the plan)
Wraps up the rope family per docs/specs/KERNEL_CONSOLIDATION_PLAN.md:

- drop the legacy ffai_/dsv4_ name prefixes; every kernel is now mt_*
  (mt_rope_llama, mt_rope_yarn, mt_rope_2d, mt_partial_rope[_rows])
- merge rope_llama_many into rope_llama.rs (decode + batched-prefill
  forms share the banded inv_freq oracle in one file)
- fold dsv4_partial_rope_rows into partial_rope.rs (same single + rows
  split). band_inv_freq is no longer imported across modules.

Kernel bodies are unchanged; names change so the FFAI emit consumer is
regenerated against mt_*. Updated the one integration test import, the
cross-referencing doc comments, and the KERNEL_AUDIT rope rows.

All 33 rope kernel tests pass (incl. merged mt_rope_llama_many).
mt_rope_llama and mt_partial_rope now take a per-row/per-token position and
an outer grid axis, so decode is just the single-row case (T=1 / n_tokens=1)
and prefill ropes every token in one dispatch. The former single-token
mt_rope_llama / mt_partial_rope kernels were redundant: #[constexpr] is a
runtime buffer read (not a baked literal, one PSO per dtype), so a
scalar-position variant bought nothing over the single-row dispatch.

- delete the two scalar-position kernels; rename the batched forms
  (mt_rope_llama_many -> mt_rope_llama, mt_partial_rope_rows -> mt_partial_rope)
- tests now cover decode (single-row), prefill (multi-row), and banding;
  benches keep both a decode and a prefill shape
- rename the integration test file/fn to drop the now-meaningless _rows
- decode callers change: pass positions=[pos] / base_position=pos with T=1

All 36 rope kernel tests pass; partial_rope integration test passes.
Applies the naming directive: kernel names describe the operation / layout,
never a model — many models share an op in different permutations, so the
differentiator is the layout. Model-specific usage notes move to comments.

- rope: mt_rope_llama -> mt_rope_banded (rope_llama.rs -> rope_banded.rs);
  the frequency-band scaling is the operation, Llama-3 / Qwen are noted as
  users in the header comment. test_rope_llama_banding -> _scaled.
- folder: kernels/conv/ -> kernels/convolution/ (spell out abbreviated
  single-word family names; keep standard acronyms gemm/sdpa/moe/rope/ssm).
- docs: KERNEL_CONSOLIDATION_PLAN (no-model-name + folder-naming rules,
  mark convolution + rope done), STYLE_GUIDE (naming example, §1 layout),
  KERNEL_AUDIT (rope row). Cross-ref doc comments updated.

cargo build + clippy clean; 36 rope + 198 conv3d tests pass.
Moves the normalization kernels out of the legacy mlx/ + ffai/ split into
kernels/norm/ and drops the ffai_ / bare-name prefixes per the consolidation
plan (names describe the operation, not the source/model):

- mlx/rms_norm, mlx/layer_norm -> kernels/norm/ (mt_rms_norm*, mt_layer_norm)
- ffai/{rms_norm_residual,rms_norm_rope,gated_rmsnorm,rms_norm_qgemv,
  gated_rms_norm_qgemv} -> mt_* (drop ffai_)
- ffai/adain1d: adain1d -> mt_adain1d
- the *_block_scaled_qgemv format matrices move as-is (already mt_<format>_*);
  the format-axis fold (plan §7) is deferred to a later pass.

Generated MSL per kernel is unchanged; only paths + inventory names change.
Updated the two integration-test imports, cross-ref doc comments, the plan
(norm marked done) and KERNEL_AUDIT rows.

cargo build + clippy clean; all norm kernel tests + the rms_norm integration
tests pass.
Moves the logits->token pipeline out of the legacy mlx/ + ffai/ split into
kernels/sampling/ and adds the mt_ prefix to the unprefixed ffai kernels:

- mlx/{softmax,sort} -> kernels/sampling/
- ffai/sampling.rs -> kernels/sampling/categorical_sample.rs
  (softmax_categorical_sample -> mt_softmax_categorical_sample; renamed the
  file off the family name to avoid module inception)
- ffai/logits_{topk,top_p,min_p,processors} -> kernels/sampling/, with mt_
  prefixes (mt_logits_topk_mask, mt_logits_top_p_mask, mt_logits_min_p_mask,
  mt_logits_temperature, mt_logits_repetition_penalty)

Generated MSL per kernel is unchanged; only paths + inventory names change.
Updated the softmax_categorical_sample integration-test imports, cross-ref
doc comments (dsv4_indexer_topk, spirv codegen), the plan (sampling done),
and KERNEL_AUDIT rows.

cargo build + clippy clean; all sampling kernel tests + the categorical-sample
integration matrix pass.
Completes wave 1: moves the core/elementwise primitive kernels out of the
legacy mlx/ + ffai/ split into kernels/ops/ per the consolidation plan.

- mlx/{arange,arg_reduce,binary,binary_two,copy,gather_axis,hadamard,
  hadamard_m,indexing,logsumexp,random,reduce,scan,scatter_axis,strided,
  ternary,unary} -> kernels/ops/ (already mt_*; inventory-registered, so no
  import sites to update beyond one snapshot test)
- ffai/{axpy_scalar_inplace,clamp_scalar,gather} + mt_vector_add.rs (renamed
  vector_add.rs) -> kernels/ops/ with ffai_ -> mt_ prefixes
- delete ffai/arg_reduce.rs: ffai_argmax was a byte-for-byte duplicate of
  mt_argmax (verified) -> real dedup
- fence.rs moves but stays intentionally undeclared (device/system fences have
  no DSL representation — the '1 out of scope' op)

Updated the steel_msl_snapshots import, the op_group/name-filter test fixtures,
the plan (ops done, wave 1 complete) and KERNEL_AUDIT rows.

cargo build + workspace clippy clean; all ops kernel tests + steel_msl_snapshots
pass.
@github-actions github-actions Bot added the ignore-for-release Hidden from release notes label Jun 12, 2026
@ekryski

ekryski commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

As per @0xClandestine's request, as relayed to me by @TheTom, closing in favour of moving rapid iteration to forks

@ekryski ekryski closed this Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ignore-for-release Hidden from release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant