Skip to content

feat(deps): replace renamed Candle fork with Mold extensions - #717

Merged
jamesbrink merged 11 commits into
mainfrom
feat/candle-extension
Aug 5, 2026
Merged

feat(deps): replace renamed Candle fork with Mold extensions#717
jamesbrink merged 11 commits into
mainfrom
feat/candle-extension

Conversation

@jamesbrink

@jamesbrink jamesbrink commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the renamed candle-*-mold packages with official Candle package identities and a narrow same-name compatibility patch
  • add mold-ai-candle for Mold-owned LTX-Video models, Stable Diffusion component policy, in-memory GGUF builders, and efficient quantization helpers
  • make the existing Flash Attention feature call candle-flash-attn directly without private RUSTFLAGS
  • publish every Mold workspace crate in dependency order, including the new mold-ai-candle and previously omitted mold-ai-scheduler
  • make crates.io publication resumable after partial releases by skipping exact versions already present and waiting for sparse-index visibility
  • document the ownership, crates.io, upgrade, and per-patch upstream exit boundaries
  • scope preview download assertions to their isolated roots so the full parallel server suite tests its real contract

Why

Renaming the fork created a second candle_core::Tensor universe, broke ecosystem interop, and forced Mold to republish three framework crates for application-owned model code. The new boundary keeps one Candle type identity while letting Mold extend public APIs in a normal workspace crate. Only backend/private changes remain in the compatibility branch.

The v0.21.0 crates.io release also exposed a publication-DAG defect: mold-ai-scheduler was omitted, so publication stopped at mold-ai-server after several immutable versions had landed. Adding mold-ai-candle would have created the same failure before mold-ai-inference. A single guarded publisher now owns the complete topological order and supports safe retries.

crates.io behavior

  • [patch.crates-io] remains a workspace/source-build concern and is not inherited by published dependents
  • published mold-ai-candle declares ordinary Candle 0.11 registry dependencies and verifies against stock crates.io Candle
  • scripts/release/publish-crates.sh publishes all ten workspace crates in dependency order with --locked
  • exact versions already on crates.io are skipped, then verified through the registry index before dependent publication continues
  • the contract test rejects missing, duplicate, or dependency-after-dependent package entries and exercises partial-release recovery without network access

Upstream work

Validation

  • cargo fmt --all -- --check
  • cargo check --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace
  • cargo check -p mold-ai --features metal,preview,expand,tui,webp,mp4,mdns
  • cargo check --manifest-path desktop/src-tauri/Cargo.toml
  • cargo test -p mold-ai-candle (6 passed)
  • cargo package -p mold-ai-candle --allow-dirty against unpatched crates.io Candle 0.11
  • bash scripts/tests/crates-publish-contract.sh
  • bash scripts/tests/release-sync-pr.sh
  • bash scripts/tests/ci-routing-contract.sh
  • bash scripts/tests/desktop-candle-lock-sync.sh
  • ShellCheck for both publication scripts
  • YAML parse for changed workflows
  • nix flake check
  • nix build .#mold-desktop --no-link
  • duplicate Candle package audit: no duplicate candle-core, candle-nn, or candle-transformers source

CUDA execution remains hardware validation; CI directly typechecks the CUDA and real Flash Attention feature path.

Closes #704
Addresses #598

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces Mold-renamed Candle packages with official Candle identities plus a narrow compatibility patch, moves Mold-owned extensions into mold-ai-candle, and makes workspace publication ordered and resumable.

  • Migrates the workspace and standalone desktop dependency graphs to Candle 0.11.
  • Adds Mold-owned LTX-Video, Stable Diffusion, GGUF, and quantization extensions.
  • Publishes all workspace crates in dependency order while skipping versions already released.
  • Adds publication-contract and lock-synchronization coverage.

Confidence Score: 5/5

The PR appears safe to merge based on the available follow-up review scope.

No blocking failure remains in the eligible review set.

Important Files Changed

Filename Overview
Cargo.toml Adds the Candle extension crate and same-name Candle compatibility patches while preserving a single upstream package identity.
crates/mold-candle/Cargo.toml Defines the publishable mold-ai-candle extension crate against ordinary Candle 0.11 registry dependencies.
crates/mold-candle/src/ltx_video/sampling.rs Introduces the standalone LTX-Video flow-matching scheduler and its timestep, sigma, and sampling operations.
crates/mold-candle/src/ltx_video/transformer.rs Moves the Mold-owned LTX-Video transformer implementation behind the new extension-crate boundary.
crates/mold-candle/src/ltx_video/vae.rs Moves the LTX-Video VAE implementation into the Mold-owned Candle extension crate.
crates/mold-inference/Cargo.toml Replaces renamed Candle dependencies with official package identities and integrates mold-ai-candle.
scripts/release/publish-crates.sh Centralizes resumable crates.io publication in workspace dependency order with exact-version checks and index-visibility waits.
scripts/tests/crates-publish-contract.sh Verifies complete, unique, topologically ordered publication and partial-release recovery.
.github/workflows/release.yml Replaces individually maintained publication steps with the guarded workspace publisher.
Cargo.lock Consolidates the graph on Candle 0.11 package identities and records the new Mold extension crate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[candle-core / candle-nn / candle-transformers 0.11] --> MC[mold-ai-candle]
  MC --> MI[mold-ai-inference]
  MI --> MS[mold-ai-server]
  MS --> MA[mold-ai]
  PUB[publish-crates.sh] --> C0[mold-ai-core]
  C0 --> DB[mold-ai-db]
  DB --> CAT[mold-ai-catalog]
  CAT --> MC
  MC --> MI
  MI --> MS
  MS --> MA
Loading

Reviews (3): Last reviewed commit: "fix(release): publish complete crate dep..." | Re-trigger Greptile

@jamesbrink

jamesbrink commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Addressed Greptile’s quantized-FLUX documentation finding in rebased commit 20032d2: both runtime-LoRA and merged/requantized GGUF loads use the Mold-owned QuantizedBypass variant (the latter without a runtime registry), while only ordinary unmodified GGUF loads use Candle’s upstream Quantized variant.

Bump candle-core-mold / candle-nn-mold / candle-transformers-mold to
0.11.1. The 0.11 API removed the LTX-Video transformer's per-layer
skip_layer_mask forward argument in favor of the set_skip_block_list
hard skip, so the STG perturbed pass now sets the block list around its
forward call — output-equivalent to the old 0/1 mask (identity
passthrough) and faster, since skipped blocks are no longer executed.
The now-dead create_skip_layer_mask helper is removed.

0.11.1 is the fork release restoring two patches the 0.11.0 upstream
sync dropped (DiagonalGaussianDistribution::mode() and high-resolution
attention-score bounding); a temporary [patch.crates-io] section points
at the local fork checkout until that tag is published, and must be
removed once 0.11.1 resolves from crates.io.
codex review caught that the desktop crate has its own Cargo root and
lockfile, which still pinned the three candle fork crates at 0.9.13 and
does not inherit the workspace's temporary [patch.crates-io]. Mirror the
temporary local-fork patch there and refresh its lockfile; both patch
sections are removed together once fork v0.11.1 is on crates.io.
@jamesbrink
jamesbrink force-pushed the feat/candle-extension branch from 2409104 to 8660099 Compare August 5, 2026 21:13
@jamesbrink

Copy link
Copy Markdown
Member Author

@greptileai

@jamesbrink

Copy link
Copy Markdown
Member Author

@greptileai

@jamesbrink
jamesbrink merged commit b25170d into main Aug 5, 2026
19 checks passed
@jamesbrink
jamesbrink deleted the feat/candle-extension branch August 5, 2026 22:26
jamesbrink added a commit that referenced this pull request Aug 6, 2026
The `mold_flash_attn_real` gate is gone (#717), but the configuration it
left behind had never been compiled by anything. `--features flash-attn`
failed `clippy -D warnings` on an unreachable expression plus a dead
static and function: the feature-on arm of `flash_attention` ended in
`return`, so the trailing math call and the whole one-shot-warning
apparatus were reachable only in a build without the feature. Split the
two paths into cfg-selected functions, which also avoids the
`needless_return` the obvious alternative reintroduces.

Make the fallback genuinely safe. `flash_is_eligible` now rejects head
dims FlashAttention v2 cannot serve. FLUX, Flux.2 and LTX-2 are pinned at
128 by their configs, but Z-Image reads `head_dim` off the loaded
checkpoint and Mold accepts arbitrary `cv:`/`hf:` exports, so an
unsupported value was user-reachable and aborted a generation the math
path would have completed.

Stop copying q/k/v into the kernel. The FFI wrapper reads outer strides
off the layout and requires only a unit stride on the last axis, so the
transposed view was always a valid argument and the three full-size
buffers per call were never required. Worth 1.4-2.2x on the flash path,
pinned bit-exact against the copy by a CUDA-gated test.

Add the `flash-attn-check` CI job. It names `mold-ai-inference` directly
because `-p mold-ai --all-targets` builds that crate as a dependency lib
and never compiles its test module, and it caches the kernel objects and
cudaforge's CUTLASS checkout so the cold build stays a once-per-cache
cost rather than an hour per run.

Measured on an RTX 4090 at BF16 with the new `attention_bench` dev-bin,
flash is 6.0-9.2x faster than math on the attention operation itself
across FLUX, Flux.2, Z-Image and LTX-2 shapes -- the operation alone, not
end-to-end generation. No release artifact enables the feature, so no
shipped binary changes behaviour.

Refs #598
jamesbrink added a commit that referenced this pull request Aug 6, 2026
#733)

Closes #598.

## The premise changed under the issue

#598 was filed against a `mold_flash_attn_real` RUSTFLAGS gate. #717
removed that gate a week later by retiring the renamed `candle-*-mold`
packages in favour of a same-name `[patch.crates-io]` branch, so the
crates.io `candle-flash-attn` now resolves its `candle-core` through the
same patch and shares one `Tensor`. `cargo check -p mold-ai --features
flash-attn` already succeeds with no special RUSTFLAGS, and `nvcc`
really compiles the FA2 kernels.

What #717 did **not** do is make that configuration compile *clean*, and
nothing in CI ever built it — which is exactly how the feature rotted
the first time.

## What this changes

**The cfg asymmetry.** `--features flash-attn` failed `clippy -D
warnings` on three counts: an unreachable expression plus a dead static
and function. One root cause — the `#[cfg(feature = "flash-attn")]` arm
of `flash_attention` ended in `return`, so the trailing math call and
the whole one-shot-warning apparatus were live only in a build *without*
the feature. The two paths are now cfg-selected functions instead of cfg
blocks sharing a body, which also sidesteps the `needless_return` that
the obvious alternative reintroduces.

**A genuinely safe fallback.** `flash_is_eligible` now also rejects head
dims FA2 cannot serve (not a multiple of 8, or above 512). This is not
defensive padding: FLUX, Flux.2 and LTX-2 are pinned at 128 by their
configs, but Z-Image reads `head_dim` off the loaded checkpoint and Mold
will load an arbitrary `cv:`/`hf:` export — so an unsupported value was
user-reachable and aborted the generation where math would have
completed it.

**Three copies per call that were never required.** The FFI wrapper
reads outer strides off the layout and requires only a unit stride on
the last axis (`candle-flash-attn-0.11.0/src/lib.rs:60-68`, strides
forwarded at `:178-190`), so the transposed *view* is already a valid
argument. Dropping the copies is worth 1.4–2.2× on the flash path alone
and is pinned bit-exact against the old copy by a CUDA-gated test.

**A CI gate.** New `flash-attn-check` job. Three details that matter:

- It names `mold-ai-inference` directly, because `-p mold-ai
--all-targets` builds that crate as a plain dependency lib and never
compiles its test module — the command that originally found this bug
cannot validate the fix. `dev-bins` is included so `attention_bench` is
type-checked too; nothing else compiles the dev-bins.
- It caches both the kernel objects and cudaforge's CUTLASS checkout —
an unretried `git clone` inside a build script that `rust-cache` does
not cover — and pins `CANDLE_FLASH_ATTN_BUILD_DIR` so a cargo
metadata-hash change doesn't silently cost another hour. cudaforge
decides freshness by content hash rather than mtime, so a restored
directory short-circuits the whole 53-kernel build.
- It runs on every `main` push and on PRs touching the dispatcher or the
feature wiring, on a filter narrower than `gpu`. Running on `main` is
not redundant: GitHub evicts caches after 7 idle days, so a
rarely-triggered job would be cold every single time.

Expect **this PR's own run to be cold** (roughly an hour): there is no
cache on `main` yet, and `save-if` restricts saving to `main`, so the
first warm run is the post-merge one. That is also the point — the job
compiles on this PR, so the gate is exercised before it is trusted.

## Measured, on an RTX 4090 at BF16

Via the new `dev-bins` `attention_bench`, at shapes taken from the
engines rather than round numbers:

| shape | math (ms) | flash (ms) | speedup | max abs diff |
| --- | --- | --- | --- | --- |
| FLUX offload 768² | 5.43 | 0.67 | 8.10× | 5.9e-3 |
| FLUX offload 1024² | 11.47 | 1.70 | 6.75× | 6.4e-3 |
| FLUX offload 1536² | 58.15 | 7.55 | 7.70× | 2.4e-3 |
| Flux.2 klein 1024² | 11.23 | 1.63 | 6.88× | 4.9e-3 |
| Flux.2 dev 1024² | 21.53 | 3.58 | 6.01× | 3.9e-3 |
| Z-Image offload 1024² | 13.64 | 1.88 | 7.27× | 3.9e-3 |
| LTX-2 stage 2 default | 114.82 | 12.99 | 8.84× | 2.9e-3 |
| LTX-2 stage 2 1024² | 175.18 | 18.99 | 9.23× | 2.4e-3 |

Two honest caveats. **This is the attention operation in isolation, not
end-to-end generation**, where attention is one term among many — do not
read 9× as a 9× faster render. And the diffs are bf16-scale (eps ≈
7.8e-3), not bug-scale: FA2 keeps an fp32 online-softmax accumulator
while the math path reduces in the input dtype, so they agree to bf16
precision by construction, not to `1e-5`.

Forcing the old `.contiguous()` copies back for an A/B gave 3.12–6.60×
instead of 6.01–9.23×, with byte-identical max-diff values in both runs.

## Deliberately not in this PR

- **The feature still ships in nothing.** `release.yml` (all five build
jobs), `Dockerfile`, the AUR PKGBUILD and the Nix flake all build
without it, so no released binary is affected by any of this. Shipping
it is a real decision rather than an oversight: compiling the feature
flips `default_backend()` to `Flash`, which would silently change every
CUDA user's output for a given seed and break the cross-backend seed
reproducibility Mold currently guarantees. If we want to ship it, the
clean shape is to ship the kernel but leave the default on `math` — that
decouples "available" from "on", and it belongs in its own PR.
- **The dispatcher's reach is narrower than the issue assumed.** Only
four call sites use it: FLUX's offload and GGUF-bypass transformers, all
of Flux.2, and Z-Image's offload transformer. Dense BF16 FLUX and
Z-Image go through upstream Candle's own attention, so `--features
flash-attn` does nothing for the most common 24 GB FLUX configuration.
Closing that means enabling `candle-transformers/flash-attn`, which
needs `candle-flash-attn` added to `[patch.crates-io]` first — the fork
declares it as a path dep, so a bare feature flip would pull a second
instance of the crate. Filed separately.
- **LTX-2 keeps its own attention.** It deliberately computes in F32 and
takes an additive mask FA2 cannot accept, so routing it through the
dispatcher is a numerics change needing video-level validation, not a
wiring change. The LTX-2 rows above exist to size that follow-up.

## Peer review

`codex review --base origin/main` raised exactly one finding, which I am
**declining** — with the reasoning, since being able to say why a
finding does not apply is the point of running it:

> **[P1] Avoid `is_multiple_of` beyond the declared MSRV**
(`attention.rs:327`) — integer `is_multiple_of` stabilized in Rust 1.87,
but `Cargo.toml` declares `rust-version = "1.85"`.

The API version is correct; the conclusion is not. Three facts against
it:

1. **It cannot be "introduced" here.** `origin/main` already has **27
occurrences across 11 files** in `crates/` (`git grep -c is_multiple_of
origin/main -- 'crates/**/*.rs'`). The effective MSRV has been ≥1.87 for
a long time; this line changes nothing about that.
2. **The suggested fix fails a required gate.** Writing `head_dim % 8 ==
0` makes `cargo clippy --workspace --all-targets -- -D warnings` fail
with `clippy::manual_is_multiple_of`, which `ci.yml` enforces. Codex's
remedy would turn a green PR red, and silencing it with an `#[allow]`
would make this one call site diverge from the other 27.
3. **Nothing verifies the declaration.** No workflow pins or tests a
toolchain (`grep -rn 'rust-version\|MSRV\|1\.85' .github/workflows/` is
empty), so the 1.85 in `Cargo.toml` is documentation, and stale
documentation at that.

The real defect Codex is circling is that **`rust-version = "1.85"` is
wrong** and has been for a while. That is pre-existing, affects
`Cargo.toml`, `CLAUDE.md` and the AUR PKGBUILD, and needs the true
minimum established by actually building on a candidate toolchain — so
it gets its own issue rather than a guess bolted onto this one.

## Verification

Local, all green: `cargo fmt --all --check`; `cargo clippy --workspace
--all-targets -D warnings`; `cargo test --workspace`; `cargo check -p
mold-ai --features preview,discord,expand,tui,webp,mp4`; `cargo clippy
-p mold-ai-inference --features flash-attn --all-targets -D warnings`
(the new gate, which fails on `main`); attention tests in *both* feature
configurations, including the CUDA-gated layout test on a real 4090;
`actionlint` on the workflow; web (1165 tests), desktop (2933 tests,
`vue-tsc`), studio (401 tests), website `verify`/`build`, and every
affected `fmt:check`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore: update Mold to Candle fork 0.11.0

1 participant