Skip to content

fix(cuda): honor explicit cuDNN algorithms and accumulate half convolutions in float - #14

Merged
jamesbrink merged 3 commits into
mainfrom
work/hunyuan3d-campaign-candle
Sep 8, 2026
Merged

jamesbrink merged 3 commits into
mainfrom
work/hunyuan3d-campaign-candle

Conversation

@jamesbrink

Copy link
Copy Markdown
Member

Two cuDNN convolution corrections from the Hunyuan3D paint campaign. Both were
qualified on branch and referenced by mold's checked-in campaign evidence, but
never merged here — so the revision mold pins (c34de9b7) cannot perform the
behavior mold's code and docs already assume.

de478c1c — accumulate half convolutions in float

Torch promotes HALF convolution compute to FLOAT while keeping HALF tensor
storage, on both of its cuDNN routes:

  • aten/src/ATen/cudnn/Descriptors.h:202-205 (v2.5.1)
  • aten/src/ATen/native/cudnn/Conv_v8.cpp:124-129

Candle passed f16 as the compute type for both Conv1D and Conv2D. Setting a
tensor-op math mode does not promote the compute descriptor — the accumulation
dtype is a separate choice — so half convolutions summed in half.

Tensor storage, the alpha/beta ABI, and every other dtype are unchanged.

5c74a518 — honor an explicit algorithm below the size heuristic

conv2d_with_algo plumbed cudnn_fwd_algo all the way to the CUDA backend,
where the dispatch arm then ignored it:

if crate::cudnn_policy::is_enabled() && cudnn_is_worth_it_2d(params, self.dtype()) {

An explicit algorithm is a numerical choice, not just a performance one — the
im2col shortcut sums differently — so a caller asking for one below the
automatic threshold silently got im2col instead. The heuristic still decides
when no algorithm was named, and the thread policy still wins over both: a
disabled policy takes im2col even with an explicit algorithm, and an
unsupported launch still falls back.

Verification

CI here is CPU-only cargo check, so it never reaches these paths. Ran the
suite on real hardware instead — 4x NVIDIA L40S, driver 595.71.05, cuDNN 9.x:

running 12 tests
...
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.31s

Three of those are new and assert actual cuDNN dispatch (not merely that the
feature compiled):

  • cudnn_f16_convolution_preserves_float_accumulation
  • cudnn_f16_conv1d_preserves_float_accumulation
  • explicit_algorithm_bypasses_size_heuristic_but_not_disabled_policy

Applying those three tests alone to this branch's merge-base — the currently
pinned c34de9b7, no source changes — reproduces the exact failures they were
written against:

assertion `left == right` failed: explicit algorithm was ignored below the threshold
  left: 0
 right: 1

F16 cuDNN accumulation relative deviation: 0.010695187

test result: FAILED. 9 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out

left: 0 is the substantive point: below the threshold an explicitly requested
algorithm dispatched cuDNN zero times. The 0.010695187 deviation reproduces
the figure recorded when this was first qualified.

https://claude.ai/code/session_01S1PVcju7evhKq3t9tEFXiW

@jamesbrink
jamesbrink merged commit bf2cd29 into main Sep 8, 2026
11 checks passed
jamesbrink added a commit to utensils/mold that referenced this pull request Sep 8, 2026
…in (#1658)

Moves the candle pin from `c34de9b7` to `bf2cd29a` (utensils/candle#14),
which
carries two cuDNN convolution corrections mold's code and docs already
assume.

## The problem

PR #1620 shipped the consumer side of the Hunyuan3D paint campaign's
cuDNN work
— `rrdbnet.rs`'s explicit algorithm selection, the
`.claude/rules/inference.md`
invariant, the changelog note, and the campaign qualification record —
but
pinned candle at `c34de9b7`, which contains neither correction. The pin
never
pointed at the revision the work was qualified against, so both
behaviors were
inert on every CUDA build since.

`docs/qualification/hunyuan3d-campaign.md:1082` states "all dependency
declarations, both Candle-containing lockfiles, and H3's backend
provenance
point to that commit". They did not.

### 1. The explicit cuDNN algorithm dispatched nothing

`rrdbnet.rs:145` asks for `ImplicitPrecompGemm` on Real-ESRGAN's first
convolution to avoid an im2col rounding difference that, in its own
comment,
"amplif[ies] through all 23 RRDBs". At the pinned revision the CUDA
dispatch arm
never read the field:

```rust
if crate::cudnn_policy::is_enabled() && cudnn_is_worth_it_2d(params, self.dtype()) {
```

That convolution is 3-channel with a 3x3 kernel, so its column buffer is
`27 x H x W` — 7.1M elements at the default 512-pixel tile, against a
35M
half-precision threshold. It cannot clear it at any supported tile size,
so the
request was discarded on every run. The upscaler runs F16 on GPU
(`upscaler/engine.rs:636`), so the second defect compounded on the same
path.

### 2. Half convolutions accumulated in half

Torch promotes HALF convolution compute to FLOAT while keeping HALF
storage on
both of its cuDNN routes (`aten/src/ATen/cudnn/Descriptors.h:202-205`,
`native/cudnn/Conv_v8.cpp:124-129`). Candle passed `f16` as the compute
type;
a tensor-op math mode does not promote the compute descriptor.

## Verification

Candle's CI is CPU-only `cargo check` and never reaches these paths, so
the
suite was run on hardware — 4x NVIDIA L40S, driver 595.71.05.

Against the **old pinned** `c34de9b7`, with only the new tests applied
and no
source changes:

```
assertion `left == right` failed: explicit algorithm was ignored below the threshold
  left: 0
 right: 1

F16 cuDNN accumulation relative deviation: 0.010695187

test result: FAILED. 9 passed; 3 failed
```

`left: 0` is the substantive result: an explicitly requested algorithm
dispatched cuDNN zero times. The `.010695187` deviation reproduces the
figure
recorded at `docs/qualification/hunyuan3d-campaign.md:630` when this was
first
qualified.

Against the **new pin**: `test result: ok. 12 passed; 0 failed`.

In this repo: `cargo check --workspace --all-targets` clean,
`scripts/tests/candle-single-identity.sh` passes on all three lockfiles.

## Blast radius

`ConvPolicy::Video` (Wan, LTX) and `ConvPolicy::Paint` select cuDNN;
`ConvPolicy::Image` stays on im2col. So convolution numerics move toward
the
Torch reference for video VAE decode and Hunyuan3D paint on CUDA builds,
and
still-image generation is byte-for-byte unchanged.

## Not closed by this

The paint qualification gates were recorded against these commits, so
this
restores the configuration they were measured under — it does not re-run
them.
The real512 VAE bound was still failing in those notes and remains open.

## Docs corrected here

- `docs/architecture/candle-extension.md:107` named the old revision as
"the
  current compatibility source" — the only stale SHA left in the repo.
- `docs/qualification/hunyuan3d-campaign.md` asserted twice that "all
dependency declarations, both Candle-containing lockfiles, and H3's
backend
provenance point to that commit", naming two Nix hashes that were never
shipped. Those claims held on the campaign branch only. Rather than
rewrite
  an evidence record, a dated correction is appended to each.

## No changelog fragment

`changelog.d/hunyuan3d-campaign.md` is still unassembled and already
carries
both notes ("CUDA half-precision convolutions", "Real-ESRGAN cuDNN
dispatch").
Neither has reached a released section, so no user ever saw the broken
claim,
and a "Fixed" bullet here would contradict two "Added" bullets in the
same
release. Labelled `skip-changelog`; this PR makes the existing notes
true.

## Nix hash

`sha256-yCwX+XdtJKY6oXp++9d9kw6opSJlRHV69Khc9LwaSQo=`, computed with
`nix store prefetch-file --unpack` on the GitHub archive. The method was
validated first by reproducing the outgoing `sha256-gInEf6…` for
`c34de9b7`.

https://claude.ai/code/session_01S1PVcju7evhKq3t9tEFXiW
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.

1 participant