Skip to content

Vulkan: pick direct convolution whenever its output check passes, not by a timing race - #38

Open
danielhanchen wants to merge 3 commits into
sync-upstream-a77d362from
vulkan-conv-direct-deterministic
Open

danielhanchen wants to merge 3 commits into
sync-upstream-a77d362from
vulkan-conv-direct-deterministic

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Stacked on the upstream sync branch (#35, sync-upstream-a77d362, now at upstream master-995-fd21ecf, commit 344a3b66). One change, in src/core/probe/conv_direct.cpp, plus the two docs that describe it (docs/backend.md, docs/backend_probe.md). No ggml patch.

Problem

On Vulkan, --conv-direct auto (the default, and what Studio sends on Vulkan) decides direct convolution with a timing race: im2col and direct run on two tiny shapes (32x32x64 and 16x16x128) and direct must win by 5%. Each probe run is 0.1 to 0.2 ms of GPU work, so on a busy machine both paths mostly measure submit overhead and the answer flips from run to run.

Published master-982-0518b34-ud331c66 Linux Vulkan bundle, Studio's Vulkan argv (--diffusion-fa), seed 42, B200 (NV_coopmat2) on a loaded host:

Run conv2d probe (im2col vs direct) Decision Z-Image Turbo 512 output
1 0.155 vs 0.103 ms on a085fa2b
2 0.207 vs 0.178 ms on a085fa2b
3 2.644 vs 2.596 ms off d81e8a5f (differs)

off was chosen in 2 of 6 runs with Studio's argv and 1 of 6 with no flags; FLUX.2 klein 4B flipped the same way (4.805 vs 4.808 ms). So a fixed seed does not reproduce, and the run that lands on off is much slower and larger:

  • AMD Strix Halo iGPU (Radeon 8060S, gfx1151, RADV, KHR_coopmat, UMA), 1024x1024, 3 runs per arm, run-to-run spread 2%:

    Model im2col (off): generate / VAE decode / peak RSS direct (on): generate / VAE decode / peak RSS
    Z-Image Turbo Q4_K_M, 8 steps 59.85 s / 14.20 s / 5.17 GB 47.78 s / 2.11 s / 1.74 GB
    FLUX.2 klein 4B Q4_K_M, 4 steps 29.55 s / 13.94 s / 5.37 GB 17.61 s / 2.15 s / 1.74 GB

    With coopmat disabled (GGML_VK_DISABLE_COOPMAT=1, the scalar shaders an iGPU without cooperative matrix support gets): VAE decode 2.72 vs 14.69 s at 1024 and 0.85 vs 1.38 s at 512. Z-Image 512 with coopmat: VAE 0.66 vs 0.97 s.

  • B200: VAE compute buffer at 512x512 is 1984 MB with im2col and 704 MB with direct; VAE decode 405 vs 333 ms of GPU time.

The gfx1151 numbers come from forcing each mode (--conv-direct on vs --conv-direct off) on the published bundle, not from a build of this branch. This branch selects what the on arm runs for 2D convolution.

Direct 3D convolution has the same race on NVIDIA (0.388 vs 0.400 ms, a tie), and on RADV its probe output check fails, so auto already turns it off there.

Fix

On Vulkan, auto no longer uses the timing:

  • 2D, VAE: direct convolution is on when the probe's output check passes (direct output within tolerance of im2col on the two probe shapes), and off when the check or the probe fails, as before. The probe runs once per shape with no time budget, so a busy host cannot exhaust the budget and flip the answer that way either. The timings are still logged.
  • 2D, diffusion model and ControlNet: im2col (conv2d direct auto: diffusion -> off (Vulkan: VAE only)). In UNet sampling direct convolution was slower on AMD's Windows driver and a tie on RADV (below), and DiT models have almost no 2D convolutions outside the VAE. --conv-direct auto,diffusion=on turns it on for the diffusion model.
  • 3D: off.

Other backends are unchanged. An explicit --conv-direct on|off or a per-module assignment still wins.

Tests

  • Build: the prebuilt Linux Vulkan leg's configuration (LunarG SDK 1.4.328.1, -DSD_VULKAN=ON -DGGML_NATIVE=OFF), all ggml patches applied on ggml c1f80e5.
  • Determinism, B200, Studio's argv, 3 runs each of Z-Image Turbo 512 (8 steps) and FLUX.2 klein 4B 512 (4 steps): conv2d on and conv3d off in 6 of 6 runs, 3 identical outputs per model, equal to the pin's output whenever its probe said on (8553f24f, 0cfb4141). In 2 of those 6 runs the probe timed direct slower (5.448 vs 7.199 ms, 2.962 vs 4.856 ms), where the old rule would have chosen off.
  • Wan 2.1 T2V 1.3B Q4_K_M (480x320, 17 frames, 10 steps), B200: renders the same file as the base (3fb6fe67); its VAE is 3D, so the 2D choice does not reach it.
  • Accuracy of direct vs im2col: at most 1 / 255 per channel, LPIPS 0.0001 (Z-Image) and 0.0000 (FLUX.2 klein) at 512x512, on both the coopmat2 and coopmat1 (GGML_VK_DISABLE_COOPMAT2=1) paths. For scale, coopmat1 vs coopmat2 alone is LPIPS 0.0008 / 0.0197.
  • On gfx1151 (a quiet runner) the pin's probe chose on in 9 of 9 runs, so there this branch renders what the pin renders when nothing else is running, and keeps doing so when something is.

Results on gfx1151 (a2c3b975, the revision before the VAE-only rule, built with the prebuilt Vulkan leg flags; base #35 in the same job, 3 interleaved runs)

  • Linux RADV and Windows 11 (AMD driver 32.0.32015.7029), Radeon 8060S: the branch prints conv2d direct auto: Vulkan0 -> on (output check passed ...) and conv3d direct auto: Vulkan0 -> off (Vulkan) on both drivers. The base's conv3d probe fails its output check on both, which is why 3D stays off.
  • On the Windows driver the base flipped between runs (im2col 0.163 vs direct 0.158 ms -> off; on in another run), and the flip changed the Z-Image pixels for one seed. The branch rendered the same pixels as the base forced to --conv-direct on, on both drivers, in every run.
  • SD1.5 F16, 512 px, 4 steps, every module on direct vs every module on im2col: on the Windows driver VAE decode 0.95-1.00 s vs 1.18-1.21 s (1.2x faster) but UNet sampling 1.64-1.66 s vs 1.45-1.50 s (11% slower); on RADV VAE 0.66 vs 1.0 s and sampling a tie. That sampling loss is why the diffusion model now keeps im2col; the VAE-only rule is otherwise the same code path.
  • test-backend-ops CONV_2D 1966/1966 and IM2COL 102/102 on Vulkan0, base and branch, both drivers.

This revision (3d7baa21, VAE only) vs a2c3b975 on the same boxes, SD1.5 F16 512 px, 20 steps, medians of 3 interleaved runs:

Driver Sampling VAE decode Total
Windows AMD 6.09 -> 5.38 s 1.02 -> 1.01 s 7.39 -> 6.66 s (1.11x)
Linux RADV 5.84 -> 5.55 s 0.67 -> 0.67 s 6.74 -> 6.45 s (1.05x)

Both drivers log conv2d direct auto: diffusion -> off (Vulkan: VAE only). The output equals a2c3b975 forced to --conv-direct off,vae=on, and --conv-direct auto,diffusion=on equals a2c3b975, at 4 and 20 steps on both drivers.

Not covered

  • Intel iGPUs (ANV), NVIDIA on Windows, macOS / MoltenVK: no hardware.
  • UNet sampling with direct convolution on NVIDIA Vulkan was not measured; the diffusion model keeps im2col there too.
  • Video on gfx1151: conv3d is now always off on Vulkan; the pin's probe already chose off in every observed run (on NVIDIA by timing, on RADV by its output check).
  • lavapipe: Mesa 23 / LLVM 15 (Ubuntu 22.04) fails the conv2d output check, so auto stays off there; on Windows (Mesa 26.2) the check passes and direct convolution was 1.8x faster in UNet sampling too, which the VAE-only rule gives up on that software device.

@danielhanchen danielhanchen self-assigned this Oct 23, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T12:38:40.894977Z 3d7baa2 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d6acac772

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +87 to +90
BackendProbeOptions options;
options.measurement_runs = 1;
options.time_budget_ms = 1e9;
const auto result = sd_run_backend_probe(backend, 0, cases, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the remaining load-sensitive probe cutoff

When a Vulkan device has less than 128 MiB free at context initialization—for example, because another workload is using VRAM—this call still inherits BackendProbeOptions::min_free_device_bytes = 128 MiB. sd_run_backend_probe then returns insufficient probe memory before checking outputs (backend_probe.cpp:116-123), and this function selects off; the same device with more free memory selects on. Thus machine load can still change the convolution path and fixed-seed pixels despite this change's determinism goal, so the correctness-only probe should remove or substantially lower that blanket threshold.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in a2c3b97: the Vulkan conv probe now sets min_free_device_bytes = 0. The probe graphs need a few MB, so the only remaining memory dependence is a device too full to allocate them at all, which still fails the probe and selects off. The changed files compile cleanly; I could not rerun the Vulkan leg on this host this round.

Comment thread docs/backend.md Outdated
Comment on lines +236 to +239
convolution graph. On Vulkan, direct 2D convolution is then enabled when its
output on two small shapes matches im2col (a failed probe defaults to `off`),
and direct 3D convolution stays `off`. The decision does not depend on timing,
so the same seed renders the same pixels on a busy and on an idle machine.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the performance guide's Vulkan auto rule

This new rule contradicts docs/performance.md:35-36, which still tells users that automatic Vulkan selection uses small benchmarks. Timing now has no effect on the 2D decision and 3D is always disabled, so users following the tuning guide receive an obsolete description of auto; update that relevant guide together with these two documentation changes.

AGENTS.md reference: AGENTS.md:L74-L76

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a2c3b97: docs/performance.md now says Vulkan auto checks direct 2D output against im2col, does not use timing, and keeps direct 3D off.

The probe graphs need a few MB; the default 128 MiB floor still turned
direct convolution off whenever another workload held the VRAM, so load
could change the path and the pixels. A device too full to allocate the
probe still fails it (off). Also update docs/performance.md, which still
described the Vulkan rule as a benchmark.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

Direct 2D convolution decodes faster in a fraction of the memory, but in UNet sampling (SD1.5, 512 px)
it ran 11% slower than im2col on AMD's Windows driver (Radeon 8060S) and tied on RADV. Auto now
turns it on for the VAE when the output check passes; the diffusion model and ControlNet keep
im2col unless asked (--conv-direct auto,diffusion=on).

This branch has not been deployed

No deployments
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