ssim2-gpu
May 2, 2026 · View on GitHub
Status: not started. The plan, gotchas, and worked example are
all written; this file is the resume point for whoever picks up the
implementation. Move it to crates/ssim2-gpu/HANDOFF.md once the
crate skeleton lands.
Read first (in order)
CUBECL_GOTCHAS.md— every cubecl-0.10-era trap with symptoms / fixes / examples. Skim end-to-end before writing the first line of cubecl code; bookmark for debugging.SSIMULACRA2_PORTING_PLAN.md— per-kernel breakdown for ssimulacra2 specifically, with sizing table and 3-day plan. Drives the work.CUBECL_PORTING_GUIDE.md— general patterns (skeleton, validation strategy, batching). Reference back when needed.crates/butteraugli-gpu/— worked example. Mirror the layout, the API shape, and thepipeline.rs/pipeline_batch.rs/kernels/structure 1:1. CopyPORT_STATUS.mdand adapt.
Resume here
The very first session should:
- Verify toolchain.
CUDA_PATH=/usr/local/cuda cargo build -p butteraugli-gpushould succeed end-to-end without errors. If cudarse fails, see G7.2 in CUBECL_GOTCHAS.md (the patches are in commitf745da9, should already be on master). If cubecl fails, verify CUDA 13 is installed (G3.1). - Scaffold the crate per
SSIMULACRA2_PORTING_PLAN.md§1:crates/ssim2-gpu/{Cargo.toml, src/{lib.rs, pipeline.rs, kernels/{mod.rs, srgb.rs, xyb.rs, downscale.rs, blur.rs, error_maps.rs, reduction.rs}}, examples/, PORT_STATUS.md, HANDOFF.md}. Copy the Cargo.toml feature layout from butteraugli-gpu. - Port
srgband writesrgb_parity.rsfirst (sub-day, bit-exact target). Get an end-to-end "I have something building and validating" win in the first hour.
What's expected to be hard
The recursive Charalampidis blur (§2 of the porting plan).
Everything else is direct translation; that one is a stateful per-
column IIR with shared-memory ring buffer, six FMAs per iteration,
and a sigma response that won't match a hand-written Gaussian. Do
not proceed past blur parity until it matches the published
ssimulacra2 crate's actual blur to ≤ 1e-5 abs on a real image.
This was the single biggest correctness lesson from butteraugli-gpu
(see G5.1, G4.5 in CUBECL_GOTCHAS.md).
Budget a full day for blur parity. If the second day is bleeding into
the third, slip day-3 work (error_maps + final orchestration) — do
not compromise on blur parity.
Toolchain reality (lifted from butteraugli-gpu HANDOFF)
- CUDA 13.2 for cubecl 0.10's CUDA backend (
/usr/local/cudasymlinked tocuda-13.2). On Blackwell GPUs (RTX 5070+, sm_120) this is mandatory because nvrtc 12.x doesn't know sm_120. - For multi-vendor validation: native Linux/Mac/Windows where the wgpu Vulkan/Metal ICD is reachable. WSL2 doesn't expose a Vulkan ICD for the NVIDIA GPU by default; only the CUDA backend works there.
- cubecl-cpu doesn't implement
atomic<u32>— not useful as a CPU validator for kernels that use atomics (i.e., the reductions).
Build commands (once the crate exists)
CUDA_PATH=/usr/local/cuda cargo build -p ssim2-gpu
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example srgb_parity
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example xyb_parity
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example blur_parity
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example error_maps_parity
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example end_to_end
CUDA_PATH=/usr/local/cuda cargo run --release -p ssim2-gpu --example parity_real_image
Cold compile time will be 5–9 minutes the first time (G6.1). Incremental rebuilds ~2 min.
Suggested next-session order
Day 1 (≈ 6–8 hours of focused work):
- Scaffold + Cargo.toml + lib.rs + kernels/mod.rs (~30 min).
- Port
srgb_to_linearkernel +srgb_parity.rs(~1 hour). Validate bit-exact againstssimulacra2::xyb::srgb_to_linear. Don't forget the LUT (256 entries, copy verbatim). - Port
xyb(linear→XYB) kernel +xyb_parity.rs(~2 hours). Thecbrtop may need thepowf(1/3)substitution; check cubecl ops list. - Port
downscale_2x_packedkernel + run it from end_to_end.rs (~1 hour). Skip the warp-shuffle plane variant — no portable WGPU equivalent. - Port
reduction(per-octave fused reduce) — start with the simple 3-launch version, fuse later (~2 hours). - Stub out
pipeline.rs::compute()with sRGB→linear→XYB→downscale pyramid + reduction (skip blur and error_maps for now). Should run end-to-end and produce some scalar that's wildly wrong — that's fine for day 1. Goal: nothing panics.
Day 2 (the hard day, ≈ 6–8 hours):
- Port the recursive Charalampidis blur. Start by copying
build.rsfrom ssimulacra2-cuda-kernel verbatim (it just generates Charalampidis coefficients intorecursive_gaussian.rs). Translate the 137-LOC IIR kernel from the CUDA Rust to#[cube]Rust (see SSIMULACRA2_PORTING_PLAN.md §2 for the kernel signature sketch). Drop the 5-plane fanout — one launch per plane. - Write
blur_parity.rsvalidating against the actualssimulacra2CPU crate's blur (path-dep withinternalsfeature if needed — see G5.1). Iterate until ≤ 1e-5 abs on a 256×256 gradient image.
Day 3 (≈ 4–6 hours):
- Port
error_mapsfused 3-output kernel (~1 hour, direct translation). - Wire the per-octave loop in
pipeline.rs::compute(): blur → pointwise multiplies for sigma11/22/12 → blur of those → runerror_maps→ reduce. end_to_end.rsshould now produce a real ssimulacra2 score. Validate against publishedssimulacra2CPU crate on a real PNG; target Δ ≤ 0.1 % score deviation.- Add
set_reference_linear/compute_with_reference_linearmirroring the butteraugli-gpu pattern. Cacheref_linear[*],ref_xyb[*],mu1[*],sigma11[*]. Validate cache drift = 0.0 vs full-compute.
Day 4+ (optional):
Ssim2Batch<R>for encoder use case. Same pattern asButteraugliBatch— pack N distorted images, batched kernels, broadcast cached reference. ~300–500 LOC depending on how many kernels need batched variants.- Cross-arch lock test (port the CPU's known-good score table).
- Multi-vendor validation: build with
--no-default-features --features wgpuon a Linux/Mac host with a Vulkan/Metal ICD.
Open questions / decision points
-
6-octave pyramid: separate buffers per level or shared pool? ssimulacra2-cuda allocates per-level (~800 MB at 1440×1080 per the CUDA crate's docstring). For the first cut, mirror that. If memory pressure becomes an issue, look at reusing the smaller octaves' buffers — but the CUDA version doesn't bother and ships fine. Don't pre-optimize.
-
Single-image vs always-multi-octave API? ssimulacra2 IS the 6-octave pyramid; there's no meaningful "single resolution" mode the way butteraugli has. So no
newvsnew_multiressplit — just one constructor. -
Score struct shape.
GpuSsim2Result { score: f64 }is the obvious shape. There's nopnorm_3analogue (ssimulacra2 has its own pnorm-like aggregation, but it's part of the per-octave reduction, not a separate exported metric). Mirror the CPU crate's return type. -
Reduction kernel: fused vs split? Each octave needs three aggregations (sum, max-norm, libssimulacra2-pnorm) per channel. Three launches per octave is simpler, ~2× slower than one fused launch. Start with three; fuse only if profiling shows it matters. (At 6 octaves × 3 channels × 3 metrics = 54 small reductions per call, the fused version probably matters more than it does for butteraugli's single reduction.)
-
Whether to ship
Ssim2Batch. Encoder-side rate-distortion loops are the big winner. The batch implementation is ~1 day of extra work. If your near-term consumer is just video metric reporting (one comparison at a time), skip it —compute_with_ referencealready cuts per-call cost by ~2×.
Score interpretation
ssimulacra2 outputs a scalar in roughly the 0–100 range (higher =
better; 100 = identical, 0 = visually broken). This is different
from butteraugli's 0–30 max-norm range. The score-from-features
weights are hard-coded constants from libssimulacra2; pull them from
the CPU crate's source as a [f64; 54] and dot-product host-side.
Files you'll touch in turbo-metrics master
New (under crates/ssim2-gpu/):
Cargo.tomlsrc/lib.rssrc/pipeline.rssrc/pipeline_batch.rs(day 4+)src/kernels/{mod, srgb, xyb, downscale, blur, error_maps, reduction}.rsexamples/{srgb,xyb,blur,error_maps}_parity.rsexamples/end_to_end.rsexamples/parity_real_image.rsexamples/batch_parity.rs(day 4+)tests/lock.rs(day 3 or later)PORT_STATUS.md,HANDOFF.md,build.rs
Optionally modified (only if you find new gotchas worth recording):
docs/CUBECL_GOTCHAS.mddocs/SSIMULACRA2_PORTING_PLAN.md
Existing crates that should NOT be touched:
crates/ssimulacra2-cuda*— keep the CUDA path running for parallel validation. Port to a new cratessim2-gpu, don't in-place rewrite.crates/butteraugli-gpu/— reference only.crates/cudarse/*— already CUDA-13-patched in commitf745da9, no further changes needed for ssim2.
Risks
| risk | mitigation |
|---|---|
| Blur IIR parity slips into day 3 | The plan assumes this; slip day-3 work, don't compromise on parity |
| cubecl-cuda's nvrtc breaks on a future CUDA point release | Pin a working CUDA version in PORT_STATUS.md; don't aggressively upgrade mid-port |
| Score weight constants drift between libssimulacra2 versions | Pin to the same CPU crate version you're validating against |
| Memory pressure at 4K/8K (per-octave allocations sum to GB) | First-cut: don't worry. If it becomes real: reuse buffers across octaves. CUDA version's docstring claims 800 MB at 1440×1080 — call it the working budget |
Where to ask for help
- The general guide and gotchas docs cover ~95 % of likely failure modes. If your bug isn't there, walk through the CUBECL_GOTCHAS.md "quick-reference checklist" at the bottom.
- For cubecl-specific questions (graph capture, stream priority, bool comptime generics), file an issue at https://github.com/tracel-ai/cubecl/issues — see #1319 as an example of the format.
- For ssimulacra2 algorithm questions, the upstream is https://github.com/cloudinary/ssimulacra2.
Cross-references
- General patterns:
CUBECL_PORTING_GUIDE.md - Comprehensive gotchas:
CUBECL_GOTCHAS.md - Detailed per-kernel plan:
SSIMULACRA2_PORTING_PLAN.md - Worked example:
crates/butteraugli-gpu/ - Original CUDA implementation:
crates/ssimulacra2-cuda/+crates/ssimulacra2-cuda-kernel/ - Open upstream blocker: tracel-ai/cubecl#1319 (CUDA stream priority — not a porting blocker, just a follow-up perf knob)