Skip to content

perf(cuda): enable hardware NVFP4/MXFP4 conversion on Blackwell builds - #1938

Merged
inureyes merged 13 commits into
mainfrom
update/issue-1934-blackwell-arch-specific
Sep 21, 2026
Merged

inureyes merged 13 commits into
mainfrom
update/issue-1934-blackwell-arch-specific

Conversation

@inureyes

@inureyes inureyes commented Sep 20, 2026

Copy link
Copy Markdown
Member

Summary

No build this project produced compiled MLX's hardware NVFP4/MXFP4 converters. The converters in mlx/backend/cuda/quantized/nvfp4_quantize.cuh are gated on __CUDA_ARCH_SPECIFIC__, every architecture list here names Blackwell plainly, so cvt.rn.satfinite.e2m1x2.f32 was compiled out of every release artifact and every CI job: block-float quantization silently took a scalar CUTLASS fallback and stochastic-rounding quantization was statically unavailable.

The fix adds the architecture-specific image to fp_quantize.cu alone, in src/lib/mlx-cpp/CMakeLists.txt, rather than to the whole mlx target. The architecture lists are unchanged.

Why not just put 121a in the list

That was this PR's first implementation, and it works. It also charges every translation unit for a converter exactly one of them reaches, because CUTLASS derives CUTLASS_ARCH_MMA_SM121A_ENABLED from the same macro. Measured on GB10, same tree, three builds differing only in how MLX was compiled:

plain 121 121a-real;121 this PR
libmlx.a 178,388,958 B +12,721,576 B (+7.1%) +49,536 B (+0.03%)
mlxcel-server 104,300,824 B +12,648,448 B (+12.1%) +65,536 B (+0.06%)
arch-specific cubin images 0 95 1
F2FP.SATFINITE.E2M1 in fp_quantize.cu.o 0 216 216
qmv.cu.o gains a second image, 3,528,478 lines byte-identical to plain
forward-JIT PTX (.target sm_121) 98 98 98

On a device the suffix matches, the architecture-specific image is the one the driver loads, not an alternative it can ignore: a marker kernel compiled under both -gencode sets prints ARCH-SPECIFIC (__CUDA_ARCH__=1210, __CUDA_ARCH_SPECIFIC__=1210) for the list form and generic for plain. So the list form does not merely add code, it replaces the code every kernel on every Blackwell host runs.

The decode regression: measured, and it does not hold

A sequential A/B reported the list form about 2.6% slower at draft width 4 with disjoint ranges. Re-measured interleaved at round granularity, one server start and one request per arm with the opening arm rotating, it does not reproduce. Full record and harness in docs/benchmark_results/data/blackwell-arch-specific-gb10-2026-09-21/.

width arm n range ratio to plain paired deltas
4 121a-real;121 5 66.04-68.18 1.0052 +0.81, +1.67, +1.54, -0.60, -1.67
4 this PR 5 65.40-67.17 0.9887 -0.26, -1.10, +1.21, -1.55, -2.08
classic 121a-real;121 5 55.94-57.24 0.9923 -1.24, +0.22, -0.09, +1.13, -2.22
classic this PR 5 55.09-57.25 0.9849 -1.18, -0.63, -0.31, -0.05, -2.14

Plain measured 65.96-67.81 at width 4 and 55.72-59.39 at classic. Every range overlaps.

The sequential result was drift, and this PR's own arm is what proves it. Its decode kernels are the same bytes as plain, verified by disassembly hash, with no second image to prefer, and it still measures 1.11% and 1.48% slower with all five paired deltas negative at classic. Five same-sign deltas at ~1.5% are reachable here with zero code difference, so the 2.6% sits inside this method's floor.

So why change the approach at all

Not for throughput, which is the honest answer. For artifact size, 257x less of it, and for blast radius: a converter fix should not replace the machine code of qmv on every sm_100, sm_120 and sm_121 user's machine, even when the one pairing measured here does not slow down. The list form also needs six workflow edits kept in sync; this needs none.

Against that, the list form is the more conventional mechanism and would automatically pick up any future MLX code gated on the same macro. cuda-blockfloat now asserts that the set of MLX files using either arch-specific macro is exactly nvfp4_quantize.cuh, so a pin bump that grows that surface fails loudly instead of silently losing coverage.

Why this is safe per translation unit

fp_quantize.cu is the only file that includes fp_quantize.cuh, which is the only file that includes nvfp4_quantize.cuh. All five __global__ kernels are in fp_quantize.cuh; the gated functions are __device__ __forceinline__ and never leave the TU that instantiates them; the object exports only ordinary host entry points (mlx::core::fp_quantize and siblings); and MLX compiles whole-program, no -rdc, so no device symbol resolves across objects. CMake fails the configure if the file ever moves.

Gates

  • cuda-blockfloat green on CI, printing F2FP.SATFINITE.E2M1 occurrences in fp_quantize.cu.o: 216, qmv.cu.o images: sm_121, fp_qmv.cu.o images: sm_121, architecture-specific cubin images in the whole archive: 1, forward-JIT-capable plain PTX images (.target sm_121): 98, MLX files gated on an architecture-specific target: backend/cuda/quantized/nvfp4_quantize.cuh, then 6 block-float tests passing on the runner
  • The same verification step run verbatim out of the workflow against real archives: passes on this PR's build, and fails on the plain build (converter absent) and on the 121a-real;121 build (qmv.cu.o carries an architecture-specific image), which is the build this PR previously proposed
  • check_cuda_arch_lists.py clean; its self-test covers 10 cases including the target-level a form, 121f, a missing per-source injection, and doc drift
  • 30-arm interleaved A/B, 0 errors, 0 NVRM delta, no CI overlap, load1 0.01-0.47
  • cargo test -p mlxcel-core --release --features cuda --lib -- --test-threads=1 global_scale: 6 passed; cuda_arch: 27 passed
  • cargo clippy -p mlxcel --lib --tests -- -D warnings at default features and cargo fmt --check clean
  • Greedy generation on three quantized checkpoints, byte-identical between the plain and 121a-real;121 builds of this tree
  • cargo check --features cuda,xla-iree --all-targets cannot run here: mlxcel-xla's build script aborts without IREE_DIST, which CI provisions as a job step

Open question, not decided here

sm_arch_with_suffix (build.rs:658-663) still appends a bare a on auto-detect, so a default local build on a Blackwell host is 121a: the whole-target form this PR argues against. With the per-source injection that suffix now buys nothing it does not already get, so I would propose dropping it for SM >= 100 and letting the injection do the work, which also makes a local build match a release build. That changes build.rs behavior and the issue put it out of scope, so it is left for review.

Closes #1934

@inureyes inureyes added status:review Under review type:performance Performance improvements priority:high High priority area:core mlxcel-core: MLX FFI, primitives, KV cache, layers platform:linux Linux (CUDA / packaging) specific labels Sep 20, 2026
MLX compiles its hardware NVFP4/MXFP4 converters only under an architecture-specific target (`__CUDA_ARCH_SPECIFIC__`, spelled `a` in CMake), so every plain Blackwell entry this repository ships or tests compiles the `cvt.rn.satfinite.e2m1x2.f32` path out and silently takes the scalar CUTLASS fallback. The difference between `90a;100;121` and `90a;100a-real;100;121a-real;121` is two tokens in a YAML env block and nothing in the build output names it, so review cannot see it.

`scripts/ci/check_cuda_arch_lists.py` parses every `MLX_CUDA_ARCHITECTURES` assignment under `.github/workflows/`, rejects any entry that is not a CMake architecture spelling, rejects the family-specific `f` suffix by name (it satisfies the dispatcher gate in `nvfp4_quantize.cuh` but not the converter gate, so nvcc fails with three errors in that header), and requires every plain Blackwell entry to have a cubin-emitting `a` sibling in the same list. Both surviving forms pass: the combined `121a-real;121` and the bare `121a`. It also requires every list in `release.yml` to appear verbatim in `docs/installation.md`, which is the drift that kept this invisible.

`scripts/ci/check_cuda_arch_lists_test.sh` drives nine throwaway roots through the checker and asserts the exit status and the operator-facing line for each. The checker fails against the current workflows, which is the point; the lists move in the next commit.

Refs #1934
Every architecture list this repository shipped or tested named Blackwell plainly (`100`, `120`, `121`), and MLX compiles its hardware NVFP4/MXFP4 converters only under an architecture-specific target, so `cvt.rn.satfinite.e2m1x2.f32` was compiled out of every release artifact and every CI job. Block-float quantization silently took a scalar CUTLASS conversion sequence instead, stochastic-rounding quantization was statically unavailable, and the tests named for MXFP4 and NVFP4 had never exercised the path they cover.

Each Blackwell target is now named twice, `<sm>a-real` beside plain `<sm>`. The architecture-specific entry contributes the cubin that carries the hardware instruction; the plain entry beside it keeps the forward-JIT-capable `compute_<sm>` PTX that a bare `<sm>a` would discard. Hopper stays `90a`, because those converters also gate on compute capability 10.0 and 9.0 cannot reach them however it is spelled.

`cuda-arch-lists` runs the new guard on every PR with no toolchain. `cuda-blockfloat` builds the shipped list on GB10, asserts with `cuobjdump` that the `sm_121a` cubin and the `F2FP.SATFINITE.E2M1` instruction really reached `libmlx.a`, and then runs the block-float quantization tests, which is the half that was missing: nothing tested the converter because nothing compiled it. The assertion is on SASS and not on PTX, because under the combined form the emitted PTX comes from the plain `compute_121` pass and takes the fallback arm, so grepping PTX for the converter returns zero on a correct build.

`docs/installation.md` gains the Blackwell rule, the archive-reading commands that distinguish the two builds, and the PTX trap. `cuda_arch_tests.rs` pins the tradeoff rather than only the constants: the `a-real` half covers its exact target and nothing else, the plain half is what still serves a later Blackwell minor by PTX, and a bare `121a` serves nothing there.

Refs #1934
`cuobjdump --dump-sass` on the 178 MB `libmlx.a` takes about ten minutes; extracting `fp_quantize.cu.o` first and disassembling that takes under a second. Measured on the plain `121` archive both report zero `F2FP.SATFINITE.E2M1`, and the single object reproduces the whole 128,920-line sm_121 disassembly the issue quotes, so the cheap form carries the same verdict. The archive-wide spelling stays in `docs/installation.md`, which is where an operator checking one build by hand will read it.

The step also prints the `F2FP.SATFINITE.E4M3` count beside it. That converter is not gated on the architecture-specific target, so it is present either way, and printing both makes "nothing compiled at all" read differently from "the fp4 gate is off".

The temporary disassembly now lives under `RUNNER_TEMP` rather than a fixed `/tmp` path, which two jobs on this self-hosted runner could otherwise collide on.

Refs #1934
Verified on this GB10 by compiling a marker kernel under both `-gencode` sets and running it: the combined form prints `ARCH-SPECIFIC (__CUDA_ARCH__=1210, __CUDA_ARCH_SPECIFIC__=1210)` and the plain form prints `generic`. So on a device the `a` entry matches, the architecture-specific image is what executes and the plain entry is purely the forward-compatibility fallback. Worth stating, because a reader can otherwise reasonably assume naming a target twice means the hardware path is merely available rather than selected.

Refs #1934
`shapeless_compile_audit_harness` carries `#[ignore = "run with scripts/audit_shapeless_compile.sh on a reachable GPU"]`, so naming it in the job's filter contributes a skipped line and no coverage. `global_scale` alone selects the six `ffi_tests` cases that drive `quantize_weights_with_mode` with `mxfp4` and native NVFP4 sidecars, which is every test in this crate that reaches MLX's block-float quantization kernels. Verified against the built test binary: the filter matches seven tests, six run and one is the ignored harness.

Refs #1934
An earlier revision of this branch raised the ceiling from 480 to 600 minutes on the reasoning that two more code-generation passes need proportionally more time. They do, and the existing ceiling already covers it: a cold six-pass build was observed at about three hours, so eight passes is about four against an eight-hour ceiling. Changing a timeout that does not need to change only adds something for review to weigh, so the number stays and the arithmetic is now written down beside it.

Refs #1934
The first run of `cuda-blockfloat` failed at the verification step with "cuobjdump not found". Everything before it was correct: the job env carried `121a-real;121`, and the build step compiled the tests for about fifteen minutes, so the list reached build.rs and CMake on the runner. Only the tool lookup failed, because neither `cuobjdump` nor `nvcc` is on PATH on GB10; the toolkit lives at `/usr/local/cuda/bin`, and the lookup tried PATH first and then nvcc's directory, which is also PATH-resolved, so both arms missed.

Both jobs now resolve the toolkit the way `src/lib/mlxcel-core/build.rs` does, `CUDA_HOME` first and then the conventional location, with PATH as a last resort rather than as the primary, and print the resolved path. That also means the gate reads the same toolkit that compiled the archive. Not-found stays a hard failure: skipping would make the gate pass while checking nothing. `cuda-sm70-compile` carried the identical lookup, latent only because its toolkit probe skips the later steps on CUDA 13, and its nvcc probe already had the absolute fallback its cuobjdump lookup lacked; both halves are now consistent.

The step also asserts that the archive it picked was configured with the list the job pins. `find | head -1` chooses among however many build directories a checkout holds, one per feature set and architecture list, so without this the gate could measure a neighbour and pass.

Verified by running the step verbatim out of the workflow, with PATH stripped of the toolkit, against real archives: it passes on the `121a-real;121` build reporting 216 fp4 conversions, 98 `sm_121` and 98 `sm_121a` cubins and 98 plain PTX images, and fails on the `121` build twice over, once on the configured-list assertion and once, with that relaxed, on the SASS count reaching zero.

Refs #1934
@inureyes
inureyes force-pushed the update/issue-1934-blackwell-arch-specific branch from fff35fb to 01fe5bb Compare September 21, 2026 01:57
MLX compiles its hardware NVFP4/MXFP4 converters only under an architecture-specific target, so every artifact this project shipped compiled them out. The fix this branch carried until now answered that from the architecture list, naming Blackwell `<sm>a-real` beside plain `<sm>`. That works and it charges every translation unit for a converter exactly one of them reaches: CUTLASS derives `CUTLASS_ARCH_MMA_SM121A_ENABLED` from the same macro, so the decode kernels get a second image too, and on a matching device that is the image the driver loads. Measured on GB10, 12.7 MB of extra archive and a different `qmv` on every Blackwell machine.

`fp_quantize.cu` is the only translation unit that can reach the converters: it is the only file that includes `fp_quantize.cuh`, which is the only file that includes `nvfp4_quantize.cuh`. All five kernels live in `fp_quantize.cuh`, the gated functions are `__device__ __forceinline__` so they never leave the TU that instantiates them, the object exports only ordinary host entry points, and MLX compiles whole-program with no `-rdc`, so no device symbol has to resolve across objects. `src/lib/mlx-cpp/CMakeLists.txt` therefore adds the extra architecture to that one source through `set_source_files_properties(... TARGET_DIRECTORY mlx ...)`, derived from whatever Blackwell entries the list already names, and fails the configure if the file ever moves.

Measured on GB10 against the same tree: the archive grows 49,536 bytes instead of 12,721,576, `fp_quantize.cu.o` carries 216 `F2FP.SATFINITE.E2M1` instructions where the plain build has none, and `qmv.cu.o` and `fp_qmv.cu.o` disassemble to the same bytes as the plain build with no second image at all.

So the architecture lists go back to plain everywhere, and an `a` in one is now the regression. The guard inverts with them: it still rejects `121f`, which does not compile, and now rejects a target-level Blackwell `a`, and additionally requires the per-source injection to still be present, because without it plain lists mean the converters are silently compiled out again.

Refs #1934
A sequential A/B on the qwen3.5-4b DFlash pairing reported the architecture-specific build about 2.6% slower at draft width 4 with disjoint ranges. Two arms measured one after the other are only as comparable as the host is still between them, so this re-measures them interleaved: one server start, one request, teardown, next binary, with the opening arm rotating every round.

It does not reproduce. Across five rounds at width 4 and five at classic, every range overlaps, the paired within-round deltas change sign, and the architecture-specific arm is nominally faster at width 4 (ratio of means 1.0052).

The third arm is what makes that conclusive rather than merely negative. It compiles the converter per translation unit, so its decode kernels are the same bytes as the plain build's, verified by disassembly hash, with no second image for the driver to prefer. It measures 1.11% slower at width 4 and 1.48% slower at classic with all five paired deltas negative. Five same-sign deltas at about 1.5% are therefore reachable on this host with no code difference at all, which is the floor the 2.6% has to be read against.

The driver imports `run_arm` from #1797's served harness rather than reimplementing the host gate, the NVRM trip wire and the DFlash diagnostics parsing, so the conditions are the same code that produced the records this compares against. Conditions were clean throughout: 30 arms, no errors, zero NVRM delta, no CI job overlapping any arm, load1 between 0.01 and 0.47.

Refs #1934
Two edges in the per-source injection, both found by driving its list parsing directly rather than by building.

A list that already names a Blackwell capability architecture-specific, `121a-real;121` for instance, would have had `--generate-code=arch=compute_121a` appended a second time, and nvcc rejects the duplicate rather than ignoring it. The workflow guard rejects that spelling, but an operator following the pre-#1934 advice to build it by hand would have hit it. The injection now skips any capability the list already covers.

`IN_LIST` needs policy CMP0057, so it works or does not depending on the `cmake_minimum_required` of whoever includes this file. `list(FIND)` has no such dependency. This file's own project declares 3.16, which would have set the policy, but the construct should not depend on that.

Driven against every list this repository builds plus the rejected forms: `90a;100;121` and `80;86;89;90a;100;120` each get exactly their two Blackwell entries, `121` gets one, `70` and `90a` get none, and the three architecture-specific spellings get none.

Refs #1934
The two job comments still described the gates as catching a plain-only Blackwell list, which is now the correct spelling. They catch the opposite: a target-level `a` entry, and a missing per-source injection. The block-float job's comment also promised the SASS assertion would survive a change of mind between the two architecture-specific forms, which is no longer the choice being made.

Refs #1934
Fifteen runs across three binaries produced one distinct completion from the 158-token prompt. It is worth stating because it rules out the arms having done different amounts of work: what the throughput numbers compare is kernel speed, not a shorter or longer generation.

Refs #1934
@inureyes
inureyes merged commit 0bbfa95 into main Sep 21, 2026
20 checks passed
inureyes added a commit that referenced this pull request Sep 21, 2026
#1946)

* chore(build): stop auto-detect at the shipped Blackwell spelling

With MLX_CUDA_ARCHITECTURES unset, `sm_arch_with_suffix` appended CUDA's architecture-specific `a` suffix to every SM from 90 up, so a default `cargo build --features cuda` on a GB10 resolved the whole MLX target to `121a`. That is the whole-target form PR #1938 decided against: every translation unit compiles under `__CUDA_ARCH_SPECIFIC__`, CUTLASS keys `CUTLASS_ARCH_MMA_SM121A_ENABLED` on the same macro, and the per-source injection in `src/lib/mlx-cpp/CMakeLists.txt` never fires at all, because an entry already carrying `a` is recorded as architecture-specific and skipped rather than given a duplicate `--generate-code`. A local build and a shipped build then differ in the machine code their decode kernels run, not merely in architecture coverage.

Both functions that spell a list from a detected capability now stop suffixing at a shared `FIRST_PLAIN_SM` boundary: `sm_arch_with_suffix` in the build script, and `CudaArchMismatch::suggested_architecture`, whose startup message was telling an operator on a Blackwell card to rebuild with `MLX_CUDA_ARCHITECTURES=121a`, the one value this repository rejects everywhere else.

Hopper keeps `90a` because `release.yml` ships `90a`. The stale rationale for it is replaced rather than repeated: upstream MLX commit `44540d12` moved `qmm_sm80`, `qmm_sm90` and `gather_gemm` to runtime NVRTC compilation and deleted the `MLX_CUDA_SM90A_ENABLED` definition the old comments cited, and `jit_module.cpp` now derives the NVRTC `--gpu-architecture` from the running device, appending `a` itself from compute capability 9 up.

`check_cuda_arch_lists.py` grows rule 6 for the auto-detect path, which rule 3 cannot see because no workflow writes that list down, and `check_cuda_arch_lists_test.sh` grows three cases for it.

Refs #1943, #1934, PR #1938.

* docs(installation): describe one CUDA architecture rule, not two

The section stated the auto-detect rule as suffixing every SM from 90 up, citing a Hopper quantized-kernel gate, and then told operators not to set `a` on Blackwell and that building `121a` is a step backwards. Both passages described the same code path in opposite terms, and `check_docs` in the guard script only verifies that the release lists appear verbatim, so nothing caught it.

It now states the rule once, the way the build script and the release workflow both spell it, and explains what `121a` costs and why the per-source injection makes it unnecessary. The Hopper paragraph records what is actually true at the current MLX pin: upstream `44540d12` moved the Hopper quantized kernel to runtime NVRTC compilation and removed the `MLX_CUDA_SM90A_ENABLED` definition this section used to cite, `90a` is kept because both release lists ship it, and `CUTLASS_ARCH_MMA_SM90A_ENABLED` still keys on `__CUDA_ARCH_FEAT_SM90_ALL` so a later pin can make it matter again.

Refs #1943.

* chore: state the Hopper cross-compile result precisely

The first pass said the quantized translation units produce identical SASS at `90` and `90a`. Measured across all five, that was too strong in one direction and too weak in another. `qmm_sm90.cu`, `qmm.cu` and `qmm_sm80.cu` emit no device function at either spelling, because upstream `44540d12` moved those kernels to runtime NVRTC compilation; their objects carry 21 lines of fatbin header and zero kernel symbols. `qmv.cu` (2898 kernels) and `fp_qmv.cu` (54) do emit device code, and it is instruction-identical, but their cubins are not byte-identical: the `90a` build adds `EF_CUDA_ACCELERATORS` to the `.headerflags` line, which is the ELF flag marking a cubin architecture-specific. So the suffix changes no instruction at this pin, which is what the decision rests on, and the text now says that rather than implying the objects match byte for byte.

Also drops the two remaining statements of the pre-#1943 rule that the new guard does not read: the inline comment in `detect_cuda_arch`, and the `90a is load-bearing` rationale on the x86_64 release job, which cited the same deleted macro. The architecture lists themselves are untouched.

Refs #1943.

* docs(build): say why the no-detection fallback stays 90a

The issue tied the `90a` fallback literal to the Hopper decision, and the cross-compile shows the suffix buys no instruction at this pin, so leaving the literal in place needed a reason rather than silence. The reason is the same one that keeps Hopper suffixed at all: `release.yml` ships `90a`, and a fallback spelled `90` would hand a host without `nvidia-smi` a build shape no published archive uses, which is the divergence this issue closed on Blackwell. The doc comment on `resolve_cuda_architectures` said only that `90a` is the last resort.

Refs #1943.

* docs(core): stop the compiled-list example showing a rejected shape

The doc comment on `compiled_cuda_architectures` illustrated the string with `"80;86;89;90a;100a-real;100;120a-real;120"`, which names Blackwell architecture-specific twice. A doc comment on a parser is where a reader copies a list from, and after this branch the project's own guard rejects that shape, so the file's example and the file's rule disagreed. It now shows the real x86_64 release list, which exercises the same parser features that mattered, pre-Blackwell plain entries and a Hopper `a` entry, and cannot drift into recommending something the build refuses. The `-real` and `-virtual` forms are still described on `parse_cuda_arch_entry` and round-tripped in `entry_display_round_trips_through_the_parser`, which is where an illustration of the grammar belongs.

The same pass corrects the header on the release-list constants in the test module, which still justified Hopper's `90a` by the quantized-kernel gate that upstream deleted.

Refs #1943.

* chore(ci): correct the reasons that named the pre-#1943 auto-detect shape

Four comments justified themselves by what auto-detection used to produce. Three CUDA jobs in `ci.yml` said their pin exists because auto-detection "yields `121a` here"; after this branch it yields `121`. Each now states the reason that survives, that a gate should name the list it compiles rather than infer it from whichever GPU the runner has, so the pin keeps a rationale instead of becoming a bare value someone deletes as redundant.

`scripts/bench_block_width.sh` is the one that inverted rather than going stale. It told a reader that a row measured on an auto-detected build is not comparable with records pinned at plain `121`, which after this branch is backwards, since an auto-detected GB10 build now resolves to exactly `121`. A wrong comparability rule in a benchmark harness produces discarded runs or published numbers someone trusts, so it now says which rows are actually suspect: those recorded between #1934 and #1943 on an unpinned build.

The aarch64 release job carried the same deleted-macro rationale the x86_64 one did, missed in the earlier pass because only one of the two was grepped.

Refs #1943.

* chore(bench): correct the reused width harness on auto-detected builds

The `draft-block-width-gb10-2026-09-20/harness/` directory is a live tool despite its dated path: the post-#1939 width sweep and the #1943 architecture A/B both re-used it, and `draft-block-width-post-1939-gb10-2026-09-21/` ships no `harness/` of its own for exactly that reason. Its four files told a reader that `build.rs` auto-detects `121a` here, so a row from an unpinned build is not comparable with the pinned records. After this branch that is backwards: auto-detection resolves to plain `121`, the same list those records pin.

Each now keeps its pin and its reason, states that the pin no longer corrects a shape but still names what was compiled, and identifies the rows that are genuinely suspect, those recorded between #1934 and #1943 on an unpinned build, rather than implying every auto-detected build is.

The record prose under `docs/benchmark_results/` and the `TECHNICAL_REPORTS` entry are left as written; they document what was true when they were measured.

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

Labels

area:core mlxcel-core: MLX FFI, primitives, KV cache, layers platform:linux Linux (CUDA / packaging) specific priority:high High priority status:review Under review type:performance Performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(cuda): enable hardware NVFP4/MXFP4 conversion on Blackwell builds

1 participant