Skip to content

feat(xla): support Molmo sparse additive visual embeddings - #918

Draft
inureyes wants to merge 16 commits into
mainfrom
feature/issue-870-molmo-xla
Draft

feat(xla): support Molmo sparse additive visual embeddings#918
inureyes wants to merge 16 commits into
mainfrom
feature/issue-870-molmo-xla

Conversation

@inureyes

@inureyes inureyes commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

  • define Molmo v1 processor-indexed sparse additive merge with authoritative negative-sentinel semantics, capacity checks, unique targets, and atomic failure
  • map the pinned OLMo decoder layout into the shared dense XLA emitter without retaining a duplicate MLX decoder
  • map Molmo fused ff_proj [value | gate] halves into the shared emitter [gate, value] argument order so SiLU is applied to the correct half
  • perform sparse visual addition in the original F16/BF16 post-scale text-embedding dtype before widening the owned prepared-prefill payload to F32
  • emit the pinned 22-layer Molmo ViT path, selected-layer concatenation, patch-mask behavior, attention pooling, and SwiGLU projector as StableHLO
  • load 647 packed vision tensors into a resident IREE auxiliary module and bind compiler, checkpoint schema, processor, crop, pool, projector, merge, and OLMo architecture identities
  • retain only the filtered MLX text embedding lookup for the approved mixed-runtime prefill boundary
  • route CLI and continuous-batch image preprocessing through an observable strict/auto/host backend policy
  • add a distinct rank-one fused-QKV bias slice after the real CLI path exposed the incompatible rank-two loader assumption
  • stage the eager MLX boundary diagnostic so patch embedding, selected ViT layers 14 and 21, and pool/projector materialize independently while flushed begin/completion markers identify a stalled stage

Validation

  • cargo fmt --all -- --check
  • git diff --check
  • cargo check --features xla-iree --lib
  • cargo clippy --fix --features xla-iree --lib --allow-dirty
  • cargo test -p mlxcel-xla molmo --lib: 14 passed
  • cargo test -p mlxcel-xla phi4 --lib: 9 passed, 5 ignored
  • root Molmo prompt contract: 1 passed
  • pinned StableHLO graph lowered with real IREE local-task: 1 passed in 5.43s
  • generic IREE auxiliary resident upload/invoke smoke passed on local-task
  • real 5.3 GB checkpoint native IREE vision -> projector -> authoritative sparse-add prepared prefill: 1 passed in 19.72s
  • cargo check --features xla-reference-diagnostics --lib after the eager stage split
  • cargo test --features xla-reference-diagnostics --lib strict_oracle_rejects_molmo2_token_scan_and_replacement: 1 passed
  • the agent sandbox CUDA attempt failed before invocation with CUDA_ERROR_NO_DEVICE; a separate escalated root CUDA run reached the production CLI Generating... boundary, then produced no token within the five-minute no-output limit and was interrupted once without retry

Draft limitations

  • the post-fix full 28-layer local-task CLI run advanced past the fused-bias loader failure, then produced no further output for 5 minutes during initial compile/upload and was stopped at the bounded diagnostic limit; it was not retried
  • the separate escalated CUDA production run likewise reached Generating... but returned no token within five minutes; it exited 130 after one bounded interruption and was not retried
  • token-exact prefill/KV/greedy output and an independent oracle are therefore not claimed by this draft
  • the earlier host MLX real-model vision run exceeded the 5-minute CPU bound; commit 6481443b now retains only selected hidden states and materializes patch embedding, selected layer 14, selected layer 21, and pool/projector at separate flushed progress boundaries, but actual cross-oracle parity remains to be collected in the next single bounded run
  • keep this PR in draft until those oracle and production-target gates are attached

Prepared-prefill parity gate

Commit d90b64e8 adds an ignored real-checkpoint host-MLX vs IREE PreparedPrefill comparison. It derives authoritative visual rows from processor indices, requires exact non-visual rows and request metadata, and checks visual rows with a documented max-abs/cosine/normalized-RMSE envelope. The fixture builds and its prompt, gate/value, and sparse-add dtype regressions pass. A single local-task checkpoint run was interrupted after the five-minute no-output bound before either parity metrics or a result were emitted; it was not retried. This PR therefore remains draft.

Staged eager diagnostic

Commit 6481443b removes the diagnostic retention of every intermediate ViT hidden state. The next actual checkpoint run reports begin/completion markers around patch embedding, the two configured selected layers in execution order, and pool/projector evaluation, so the five-minute no-output policy can stop at a named stage and a completed run can proceed directly to first-divergence comparison. The full checkpoint gate was intentionally not rerun in this commit.

Dedicated CUDA eager boundary runner

Commit c6c8f957 exposes the existing pinned Molmo boundary comparison as the standalone xla_molmo_reference_check example. The eager reference is forced onto MLX CUDA while IREE remains explicitly pinned to local-task or local-sync. Flushed stage markers and configurable 30-second heartbeats cover patch embedding, each selected ViT layer, and pool/projector materialization; the comparison stages and EXACT/VISION tolerances are unchanged.

Build from the repository root:

IREE_DIST=/home/inureyes/Development/mlxcel/spike/iree-ffi/iree-dist \
cargo build --release --example xla_molmo_reference_check \
  --features cuda,xla-reference-diagnostics

Run the pinned gate:

MLXCEL_DEVICE=gpu \
MLXCEL_MOLMO_FIXTURE=/home/inureyes/models/molmo-7b \
MLXCEL_MOLMO_IMAGE=tests/fixtures/test_image.png \
MLXCEL_MOLMO_IREE_DEVICE=local-task \
MLXCEL_MOLMO_HEARTBEAT_SECS=30 \
./target/release/examples/xla_molmo_reference_check

The standalone Rust/IREE feature boundary passed cargo check --example xla_molmo_reference_check --features xla-reference-diagnostics and git diff --check. The CUDA release build was intentionally stopped during the shared MLX CUDA C++/NVCC rebuild after resource contention with other epic worktrees was identified; the actual 5.3 GB checkpoint gate was not rerun. The PR remains draft pending one bounded run of the command above.

2026-07-27 rebase and runner audit

  • rebased onto origin/main at ec6fbb6d; the only conflicts preserved the merged numeric_ops/xla-micro-oracle foundation alongside the Molmo modules and device-neutral reference feature
  • commit a4047b22 restores default-library compilation by moving Molmo's scalar F16/BF16 merge rounding out of the IREE/test-only weight-loader module while retaining the existing bit-contract tests
  • the local 5.0 GB checkpoint, pinned image, config/preprocessor/processor SHA-256 values, GB10 CUDA device, and local-task IREE distribution are present; the fixture lacks Hugging Face metadata, so the runner invocation now states the already-supported pinned MLXCEL_MOLMO_REVISION=5c04b3a418979597b1968e41414ad799c87533e8
  • one GB10 sm_121a release build of xla_molmo_reference_check compiled its dependencies but did not finish the final GNU root build/link within the five-minute bound; it was interrupted once with exit 130, produced no executable, and this host has no lld or mold alternative
  • default mlxcel-xla Clippy passed; focused Molmo tests passed 15/15; F16 conversion tests passed 4/4; formatting and diff checks passed

No actual checkpoint comparison ran at this head, so intermediate, KV/logit, greedy-token, and lifecycle acceptance remain open. The PR stays draft and the unqualified artifact marker remains unchanged.

Closes #870

@inureyes inureyes added area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata priority:low Low priority status:review Under review type:enhancement New features, capabilities, or significant additions labels Jul 24, 2026
@inureyes
inureyes force-pushed the feature/issue-870-molmo-xla branch from ac54a07 to 2c6c9d4 Compare July 24, 2026 10:22
@inureyes

Copy link
Copy Markdown
Member Author

Bounded actual-checkpoint follow-up on commit 6481443: all pinned processor boundaries passed exactly (677,376 patch values, 1,152 mask values, image token IDs, sparse image_input_idx, and grid metadata). The staged eager capture then materialized the patch embedding successfully and entered selected ViT layer 14. That named stage produced no completion marker within the five-minute no-output limit and was interrupted once with exit 130. The CPU run will not be repeated unchanged; the remaining numeric oracle requires a small GPU eager diagnostic runner or a cached/staged equivalent. No parity result is claimed, and the PR remains draft.

@inureyes

Copy link
Copy Markdown
Member Author

Rebased onto main@7fe1412d and force-pushed as d20b06e5.

Post-rebase validation:

  • cargo fmt --all -- --check
  • git diff --check
  • cargo test -p mlxcel-xla molmo --lib: 15 passed
  • cargo check -p mlxcel-xla --features iree: passed with existing warnings

The Molmo runtime now uses the explicit new_legacy_unqualified auxiliary-artifact path. The prior real-checkpoint run never completed its boundary report, and independent token-exact/KV/greedy acceptance remains absent. CUDA is currently unavailable on this host, so no new actual result is claimed. This PR remains draft and #870 remains blocked by #932.

inureyes added 15 commits July 27, 2026 12:49
Preserve processor-supplied image_input_idx row correspondence across negative sentinels and reject duplicate, out-of-range, over-capacity, malformed, or non-finite inputs before native execution.

Keep patch-mask rows distinct from pooled feature rows and prove an embedding-replacement negative fixture differs from the required additive result.

Refs #870
Preflight every destination sum so non-finite overflow cannot partially mutate a reusable batch slot, and reject processor mask values outside the qualified [-1,1] coverage-and-sentinel domain.

Keep raw patch-mask rows distinct from pooled feature rows and split tests out of the production module to preserve file-size limits.

Refs #870
Reuse the shared XLA dense language graph for Molmo v1 by binding its OLMo-style checkpoint names, interleaved RoPE, fused QKV bias, and fused SwiGLU projections without constructing a second decoder.

Interpret Molmo intermediate_size as the combined gate/up width and test every fused row slice against the checkpoint schema.

Refs #870
Load the pinned Molmo v1 vision, pooling, and projector weights into one auxiliary IREE module, retain only the MLX text embedding table for prepared prefill, and route CLI/server preprocessing through an observable backend policy.

Bind static dimensions and OLMo architecture into the artifact identity, validate allocation arithmetic before invocation, and cover the replacement-vs-addition logit sentinel.

Refs #870
Represent Molmo's rank-one fused QKV bias with a distinct element-slice schema instead of passing it through the rank-two projection row slicer.

Reject incompatible Molmo architecture variants before graph construction so the pinned OLMo layout cannot silently drift.

Refs #870
Map Molmo's fused FFN value and gate halves into the shared emitter's gate and value argument order so SiLU is applied to the correct half.

Perform sparse visual addition in the original F16 or BF16 text-embedding dtype before widening the owned prepared payload to F32.

Validation: cargo test -p mlxcel-xla molmo --lib; cargo test -p mlxcel-xla phi4 --lib; cargo clippy --fix --features xla-iree --lib --allow-dirty.

Refs #870
The existing real-checkpoint gate compared only the final PreparedPrefill values, so processor, vision, projector, sparse-merge, and decoder propagation regressions could not be localized.

Add a CPU local-task reference feature and a diagnostics-only Molmo graph that captures pinned processor contracts, patch embeddings, selected vision layers, projected features, prepared sparse-add rows, and one resident decoder's KV and logits boundary. Negative controls reject Molmo2 token scanning and replacement semantics without loading a duplicate decoder.

Validation: Molmo unit tests 15/15; CPU diagnostic tests 2/2 with the actual-checkpoint test ignored; diagnostic StableHLO compiled for IREE local-task; feature clippy completed with pre-existing unrelated warnings.

The pinned 5.3 GB actual-checkpoint gate was not run in this task environment.

Refs #870
Materialize the eager MLX patch embedding, each selected ViT layer, and the pool/projector at explicit diagnostic boundaries so the pinned #870 gate reports progress before and after every potentially long stage.

Retain only configured selected hidden states instead of every ViT layer, bounding the lazy graph and allowing the next actual checkpoint run to identify the first stalled or divergent stage under the five-minute no-output policy.

Validation: cargo check --features xla-reference-diagnostics --lib; cargo test --features xla-reference-diagnostics --lib strict_oracle_rejects_molmo2_token_scan_and_replacement; cargo fmt --all -- --check; git diff --check. The actual full-checkpoint gate was intentionally not rerun.

Refs #870
Expose the pinned Molmo reference gate as a dedicated example so the eager oracle can run on MLX CUDA while IREE remains on local-task or local-sync. Emit flushed stage progress and periodic heartbeats without changing the existing comparison thresholds.
Keep the rebased Molmo runtime on the explicit legacy artifact path until the real-checkpoint boundary oracle completes.\n\nRefs #870
Rebasing the Molmo prepared-prefill contract onto the numeric-oracle foundation exposed that its F16 and BF16 merge rounding called through a weight-loader module compiled only for IREE or tests, which broke the default mlxcel-xla library build.

Move the three scalar conversion helpers into a feature-neutral module while retaining the weight-loader re-exports and existing bit-contract tests. Preserve both diagnostic feature families during the rebase, and document the pinned revision override required by the locally complete checkpoint fixture.

Validation: default mlxcel-xla Clippy, 15 focused Molmo tests, four F16 conversion tests, formatting, and diff checks.

Refs #870
@inureyes
inureyes force-pushed the feature/issue-870-molmo-xla branch from d20b06e to a4047b2 Compare July 27, 2026 04:09
@inureyes

Copy link
Copy Markdown
Member Author

Rebased #918 onto origin/main@ec6fbb6d and pushed a4047b22. The only conflicts were narrow unions of the merged numeric-oracle foundation with the Molmo module and reference-diagnostics registrations.

The actual-run assets are present and pinned: the 5.0 GB checkpoint, image and config hashes, GB10 CUDA device, and local-task IREE distribution all match. The local fixture lacks Hugging Face metadata, so the runner documentation now supplies its already-supported pinned revision environment value.

One sm_121a release build reached the final GNU root build/link but exceeded the five-minute bound, was interrupted once with exit 130, and produced no executable. This host has neither lld nor mold; no identical retry or speculative build change was attempted. Consequently no actual checkpoint comparison ran.

Validation completed: default mlxcel-xla Clippy, Molmo 15/15, F16 conversion 4/4, formatting, and diff checks. The PR remains draft; intermediate, KV/logit, token-exact, and lifecycle acceptance are still required.

@inureyes

Copy link
Copy Markdown
Member Author

Additional bounded linker audit after the rebase evidence:

  • /usr/bin/ld.gold is present even though lld/mold are not.
  • A target-linker override and a final-example cargo rustc override were each attempted once. Both stopped in about 15 seconds, before final link or model execution, because changing the linker invalidated Cargo fingerprints; mlxcel-core then saw cached=none, purged that new build output's MLX _deps, and the sandbox could not resolve the pinned MLX clone.
  • No executable was produced, no checkpoint comparison ran, and no repository file changed. The temporary linker wrapper was removed.

This does not alter the qualification result: #918 remains draft/fail-closed pending a reproducible cached release runner and the original intermediate/KV/token/lifecycle gates.

Refs #870
Refs #932

The Molmo v1 reference gate created its production IREE preprocessor
without first applying the diagnostics-only local-task thread
configuration added in #945. `configure_diagnostic_local_task_threads`
had no callers on this branch, while the Gemma3 runner already applies
it, so the Molmo runner aborted before any comparison ran:

    xla_aux_create failed (status 13):
    iree/base/threading/thread_pthreads.c:159: INTERNAL;
    thread creation failed with 22

IREE parses its process-global flag registry when the first instance is
created, so the call has to precede `load_iree` to have any effect.

With this, the pinned 5.0 GB checkpoint gate completes end to end for the
first time on GB10: processor stages exact, patch embedding, selected
layer 14, projector, both prepared sparse-add merge stages, layer-0 KV,
all-layer KV and prefill logits all pass, leaving 4 of 1,181,696 values
outside tolerance at selected layer 21.

Refs #870
@inureyes

Copy link
Copy Markdown
Member Author

First completed actual for this gate, at a954d214

The pinned 5.0 GB checkpoint gate has now run end to end on the GB10 host
(driver 580.159.03, CUDA 13.0, IREE 3.12.0rc20260721, Rust 1.93.1, MLX pin
b7c3dd6d27f4). Two separate things were blocking it, one environmental and one a
real defect on this branch.

1. The release build was never actually failing

cargo build --locked --release --example xla_molmo_reference_check --features cuda,xla-reference-diagnostics completes in 10 minutes 34 seconds from a cold
MLX CUDA tree. The earlier interruption at five minutes was a budget that was too
short for a from-scratch MLX CUDA build plus the GNU root link, not a linker or
toolchain problem. No lld or mold is needed. The incremental rebuild after the
code fix below took 2 minutes 52 seconds.

One prerequisite: this host needs MLX_CUDA_ARCHITECTURES=121a exported before the
build. src/lib/mlxcel-core/build.rs resolves the architecture as
MLX_CUDA_ARCHITECTURES else detect_cuda_arch() else the literal "90a", and
detect_cuda_arch() shells out to nvidia-smi. Where the GPU is not exposed to the
build, it silently produces 90a binaries that cannot launch on sm_121, failing later
with invalid resource handle or no kernel image is available for execution on the device. That is a build-environment failure and is recorded separately from numerics.

2. Real defect: the #945 local-task configuration was never applied here

The runner created its production IREE preprocessor without applying the
diagnostics-only local-task thread configuration. configure_diagnostic_local_task_threads
had no callers on this branch, while the Gemma3 runner already applies it before its
first IREE instance. The gate therefore aborted before any comparison:

xla_aux_create failed (status 13):
src/runtime/src/iree/base/threading/thread_pthreads.c:159: INTERNAL; thread creation failed with 22

followed by Destroy(handle_) failed: driver shutting down and SIGABRT (exit 134).
a954d214 calls it immediately before load_iree. IREE parses its process-global flag
registry when the first instance is created, so the ordering is load-bearing.

Result

MLXCEL_DEVICE=gpu, MLXCEL_MOLMO_FIXTURE=/home/inureyes/models/molmo-7b,
MLXCEL_MOLMO_REVISION=5c04b3a418979597b1968e41414ad799c87533e8,
MLXCEL_MOLMO_IMAGE=tests/fixtures/test_image.png, unchanged tolerances
atol=0.25, rtol=0.05, no override.

stage status elements max_abs failures
processor.patches PASS 677376 0.0 exact 0
processor.patch_masks PASS 1152 0.0 exact 0
processor.image_token_ids PASS 316 0
processor.image_input_idx PASS 288 0
processor.grid PASS 3 0
vision.patch_embedding PASS 1179648 1.336575e-3 0
vision.selected_layer_14 PASS 1181696 2.258102 0
vision.selected_layer_21 FAIL 1181696 2.317841 4
vision.projector PASS 1032192 2.251205e-1 0
prepared.non_visual_rows PASS 132608 0.0 exact (atol=0, rtol=0) 0
prepared.sparse_add_visual_rows PASS 1032192 2.500000e-1 0
decoder.layer0_kv PASS 128 0.0 exact 0
decoder.all_layer_kv PASS 3584 5.212402e-2 0
decoder.prefill_logits PASS 152064 1.176004e-1 0

Exit 101 is the intended first-divergence panic on the single failing stage.

Four values out of 1,181,696, and the divergence does not propagate: the projector that
consumes the ViT output, the authoritative sparse-add merge, all-layer KV and the prefill
logits all pass downstream of it. elements=1181696 is 2 crops times 577 tokens times
1024 hidden, so flat index 514613 is crop 0, token 502, channel 565.

The 4 values are deterministic, and the gate is cross-device by construction

Re-running with MLXCEL_MOLMO_IREE_DEVICE=local-sync, which is single-threaded and
deterministic, reproduces the failure identically: same max_abs=2.317841, same
max_rel=5.661761e3, same 4 failures, same first_failure=514613. So this is not a
local-task worker or reduction-order artifact.

MLXCEL_MOLMO_IREE_DEVICE=cuda is rejected by the gate itself
(the #870 reference gate requires IREE local-task or local-sync), so the comparison is
by design MLX on CUDA against IREE on CPU. The two selected layers straddle the threshold
narrowly under atol + rtol*|expected|:

  • selected_layer_14 max difference 2.258102 passes, its worst element sitting where
    |expected| is at least about 40.2
  • selected_layer_21 max difference 2.317841 fails, its worst element sitting where
    |expected| is below about 41.4

Both layers differ by roughly the same amount; layer 21's worst element simply lands at a
slightly smaller |expected|. That looks like the residual of a GPU reference against a
CPU candidate rather than a semantic mismatch, especially since every downstream production
stage including the prefill logits passes. Deciding it properly needs the comparison to run
with both sides on the same device, which this gate currently forbids.

No tolerance was changed and no production arithmetic was touched. Greedy token exactness,
CLI and server paths, cancellation and slot reuse, and mixed batching were not reached
because the strict first-divergence panic stands. This PR stays draft.

@inureyes

Copy link
Copy Markdown
Member Author

Correction to the numbers above: the reference was TF32, not F32

The results I posted earlier were collected with MLX's default MLX_ENABLE_TF32=1, which
selects CUBLAS_COMPUTE_32F_FAST_TF32 (10-bit mantissa) for f32 contractions.
src/lib/mlxcel-xla/README.md declares that mode is not valid F32 reference evidence. The
gate does not set the variable and neither did I, so those numbers compared a TF32 MLX
reference against a true-F32 IREE candidate. Re-running at a954d214 with
MLX_ENABLE_TF32=0, everything else identical:

stage TF32 default MLX_ENABLE_TF32=0
vision.patch_embedding 1.336575e-3, 0 fail 1.336575e-3, 0 fail
vision.selected_layer_14 2.258102, 0 fail 1.991226, 0 fail
vision.selected_layer_21 2.317841, 4 fail 2.046043, 4 fail
vision.projector 2.251205e-1, 0 fail 1.643753e-1, 0 fail
prepared.sparse_add_visual_rows 2.500000e-1, 0 fail 1.875000e-1, 0 fail
decoder.all_layer_kv 5.212402e-2, 0 fail 5.070114e-2, 0 fail
decoder.prefill_logits 1.176004e-1, 0 fail 1.173029e-1, 0 fail

True F32 removes about 12 percent of the drift at both selected ViT layers and 25 to 27
percent at the projector and the sparse-add merge. The patch embedding is unchanged, which
matches mlx/backend/cuda/conv.cpp:139 gating the convolution path on enable_tf32()
separately.

It does not close the gate. The same four elements at flat index 514613 still exceed
atol=0.25, rtol=0.05, and every other stage still passes. So the residual at
vision.selected_layer_21 is now attributable to the graph rather than to the reference
policy, which is a cleaner statement of the remaining blocker than the earlier numbers
supported.

Two further observations on that residual, both from runs at this head:

  • It is device independent. local-task, local-sync and cuda all produce 4 failures at
    the same flat index 514613 with max_abs 2.317841, 2.317841 and 2.316116 respectively
    under the TF32 default. IREE agrees with itself across CPU and GPU, so the variation is not
    an IREE backend or reduction-order artifact.
  • It is not a precision-policy mismatch of the kind seen in the Qwen families. This
    checkpoint has torch_dtype: float32 with an unquantized vision tower
    (skip_vision_non_divisible: true), and the Molmo emitter applies no Precision demotion,
    so both sides are plain f32 by construction.

MLXCEL_MOLMO_IREE_DEVICE=cuda currently trips the gate's own assertion at line 122. I
tested it by widening that assertion locally and reverted the change, since it fixes nothing;
the result is recorded above only as evidence that the residual is device independent.

Filed the reference-policy issue and the two build.rs architecture defects on #932.
This PR stays draft.

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

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata priority:low Low priority status:review Under review type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(xla): support Molmo sparse additive visual embeddings

1 participant