Skip to content

sm75: VAE encoder, runtime LoRAs, quantized tiers and a q8_fp16 attention kernel on Turing - #13

Merged
xycld merged 12 commits into
mossimage-cpp:mainfrom
CloverIris:sm75-lora-vae
Nov 8, 2026
Merged

xycld merged 12 commits into
mossimage-cpp:mainfrom
CloverIris:sm75-lora-vae

Conversation

@CloverIris

@CloverIris CloverIris commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Ten commits that make the sm_75 (Turing) build a full engine, verified on an RTX 2080 8 GiB with the SFT bundle.

1. The VAE encoder runs on Turing (img2img, inpainting, reference images, hires fix)

  • encoder_conv2d/conv2d_down dispatch to fp32 SIMT kernels on cc<8, including a new stride-2 3x3 downsample
    kernel; the not_supported refusals for these features are gone.
  • Measured: a 256^2 encode costs about 0.19 s and the 384^2 hires pass about 0.5 s.

2. Runtime LoRAs on Turing (merged instead of side branches)

  • The request's placement is forced to merge on cc<8 (the side-branch GEMMs are sm_80+ CUTLASS); the merge kernels
    are plain SIMT and run before the bf16 -> fp16 conversion. A strength change reloads the DiT instead of re-merging
    into fp16 weights.
  • On INT8 ConvRot DiTs the merge requantizes (the usual warning); turbo + --lora now refuses up front with the
    reason and the working alternatives.

3. Performance (Nsight Systems + Nsight Compute on the RTX 2080)

  • VAE conv kernel: float4 shared loads/accumulators, then two output pixels per thread (Nsight Compute showed it
    L1/TEX-bound). Decode 0.37 -> 0.20 s at 256^2, 10-12 -> 2.19 s at 1024^2; encode 0.39 -> 0.19 s.
  • VAE mid attention: tiled over 32 query rows per block with compile-time lane counts (the untemplated version spilled
    to local memory). 5.4 -> 0.24 s at a 1024^2 latent, bit-identical output.
  • Exact attention: Q fragments loaded straight from global memory (three blocks per SM instead of one) and eight
    warps for head_dim 256. 1024^2 call 32.8 -> 14.3 ms (7.3 TFLOPS); text encoder 29.4 -> 17.3 ms.
  • gemm_out_f32 and the bf16->fp16 row conversions vectorized.

4. Quantized tiers

  • moss-infer quantize --format int4 writes the Turbo tier's convrot_w4a4 format (packed 4-bit weights, per-row
    scale, 256-column rotation groups, _quantization_metadata); the scale rule is comfy-kitchen's clipping search
    (--recipe mse, the default) or absmax. The produced 2.83 GB file matches a published community file's dequantized
    weights at cosine 0.9993.
  • New Turing q8_fp16 attention kernel (src/kernels/sm75/attention_q8_sm75.cu): per-row INT8 q/k, m8n8k16 QK^T, the
    exact kernel's fp32 softmax and fp16 P·V, wired through attention_q8.cu, so tier high runs on a bf16 DiT.
    It matches the exact path at cosine 0.9986. No speedup on Turing: INT8 m8n8k16 has the same MACs per instruction
    as fp16 m16n8k8 (Ampere's m16n8k32 is the 2x part), so this is a bandwidth path on this architecture.

Verified on the RTX 2080 8 GiB

Configuration 256^2, 8 steps 1024^2, 2 steps Device peak
reference (bf16) 7.50 s (0.94 s/step) 8.60 s (4.3 s/step) 6.37 GiB
high (INT8 ConvRot 5.04 GB) 0.99 s (0.12 s/step) 5.18 s (2.54 s/step) 5.27 GiB
turbo (INT4 convrot_w4a4 2.83 GB) 1.14 s (0.14 s/step) 5.16 s (2.57 s/step) 3.06 GiB
high on a bf16 DiT (q8_fp16) 7.64 s 8.83 s 6.46 GiB

Also verified: --init-image, --reference, --hires, merged LoRA on bf16 and INT8 DiTs, quantize INT8 and INT4
round trips, sm75_check all green, and the MOSSINFER_SM75=OFF build unchanged. docs/sm75.md and
docs/sm75-quantization.md carry the details and the measured numbers.

- encoder_conv2d and conv2d_down dispatch to fp32 SIMT kernels on cc<8, with a new stride-2 3x3
  downsample kernel in kernels/sm75/vae_sm75.cu. img2img, inpainting, reference images and the hires
  fix now work on RTX 20xx instead of being refused with not_supported.
- Runtime LoRAs are merged into the weights at load on sm7x: the fused side-branch GEMMs are sm_80+
  CUTLASS, but the merge kernels are plain SIMT, so the stack is forced to Placement::kMerge before the
  bf16-to-fp16 conversion. A strength change drops the DiT and reloads it (the resident weights are fp16,
  so an in-place re-merge would mix dtypes) instead of re-merging.
- lora_merge.cu joins the portable sm75 sources; docs/sm75.md updated (only control adapters remain
  refused).

Verified on the RTX 2080: --init-image, --reference and --hires produce images; --lora reports its 6
adapters as merged and changes the image; the reference-tier apple renders correctly with and without a
LoRA.
Profiling on the RTX 2080 (Nsight Systems) showed the VAE convolutions as the largest single kernel
(52.6 % of the img2img GPU time, 10.7 ms per launch) and the sm75 GEMM output/bias conversion at
176 ms per four-step generation.

- conv2d_f32_sm75: float4 shared-memory loads and float4 accumulators (four input channels per FMA),
  vectorized float4 output stores with the bias and residual added in the same order. VAE decode
  0.37 -> 0.20 s at 256^2 (10-12 -> 7.8 s at 1024^2), VAE encode 0.39 -> 0.19 s.
- gemm_out_f32: float4 accumulator loads and packed bf16 stores, 176 -> 93 ms per four-step run.
- bf16_to_f16_rows_scaled: pair-wise bf16 loads and packed half2 stores in both passes.
…ps for head_dim 256

The exact attention kernel staged the 64x64 Q tile in shared memory, which left one block per SM and four warps
running. The Q fragments are now loaded straight from global memory (bf16 -> fp16 per lane), the block's shared
memory is only the K and V tiles (about 19 KiB), and three blocks fit an SM. head_dim 256 (the text encoder's
causal attention) uses eight warps per block.

Measured with tools/sm75_check bench (RTX 2080): the DiT's 1024^2 attention call 32.8 -> 14.3 ms (3.2 -> 7.3
TFLOPS), the 256^2 call 0.23 -> 0.13 ms, the text encoder's 4096-token causal attention 29.4 -> 17.3 ms (7.9
TFLOPS). End to end at 1024^2 the denoise step drops from 6.4 to 5.2 s (two steps: 28.2 -> 23.6 s); 256^2 is
unchanged within noise. tools/sm75_check gains a bench command for these shapes.
…ane counts

The VAE mid attention read K and V for every query row: at a 1024^2 latent (16384 tokens) that is 8 TB of reads
and 5.4 s of the decode. The kernel now keeps 32 query rows per block in registers (four per warp) and stages the
K and V tiles once per block in shared memory, so each tile is read L/32 times. The lane count is a template
parameter: with a runtime D the per-row arrays landed in local memory and the kernel was 20x slower.

Measured at 1024^2 (RTX 2080): VAE attention 5.4 s -> 1.6 s, decode 7.6 s -> 3.3 s, one-step total 17.4 -> 14.0 s.
The decoded PNG is bit-identical to the previous build (same key order in the online softmax). sm75_check vae still
matches its host reference.
Nsight Compute showed the conv kernel L1/TEX-bound at 93 % with 40 % compute throughput: the weight tile was
reloaded for every output pixel. Each thread now computes two pixels eight rows apart, so each weight float4
serves two pixels and the shared-memory traffic per FMA halves. t=1024^2 VAE decode 3.30 -> 2.19 s, bit-identical
output.
New docs/sm75-quantization.md: what reference/high/turbo require, the measured RTX 2080 numbers for the
three tiers with quantized DiT files (0.12 s/step INT8, 0.14 s/step INT4 at 256^2), the gaps (no INT4
writer in quantize, high needs a quantized DiT on Turing because there is no 8-bit attention, LoRA on
quantized tiers unverified) and a five-point plan (INT4 output, Turing q8_fp16 attention, a tier rule
fallback, LoRA on quantized tiers, docs/app). docs/sm75.md links it and states the quantized-file
requirement.
moss-infer quantize --format int4 produces the file the turbo tier loads: packed 4-bit weights ([N, K/2],
even column the low nibble), a per-row f32 scale, 256-column ConvRot rotation groups and the file-level
_quantization_metadata the loader reads (convrot_w4a4, convrot_groupsize 256, quant_group_size 64). The scale
rule is the format's clipping-ratio search (--recipe mse, the default) or absmax (--recipe rtn); LoRA merging
and --keep-bf16 work as for int8.

Verified on the RTX 2080: the 5B DiT becomes a 2.83 GB file (55 s with rtn, 283 s with mse); against a
published community convrot_w4a4 file the dequantized weights match at cosine 0.9993 and the bf16 tensors are
bit-identical, and the produced file runs tier turbo (0.16 s/step at 256^2, 3.9 GiB peak).
Verified on the RTX 2080: reference + --lora merges into bf16; high + --lora merges into the INT8 ConvRot
weights (merge_requantize_int8) and reports the requantization warning; turbo + --lora used to fail with the
generic merge error, whose advice (lora_placement side_branch) does not exist on Turing. The pipeline now
refuses that combination up front on sm_7x and names what works (tier high or reference).
Records the dispatch points (attention_q8.cu's three functions), the workspace layout, the m8n8k16 fragment
mapping from CUTLASS's sm75 header and the fact that the exact kernel's skeleton carries over, so the kernel
work can start from the document.
New src/kernels/sm75/attention_q8_sm75.cu: per-row INT8 q/k (a quantize pre-pass, v converted to fp16), QK^T
with Turing's m8n8k16 INT8 MMA, then the exact kernel's fp32 online softmax and fp16 P·V. attention_q8.cu's
three entry points (supported, attention_q8_workspace_bytes, attention_q8) dispatch to it on cc 7.5, and
dit_block_quant keeps the separate qk_norm_rope_moss on Turing (the fused pre-pass is sm_80-only).

Verified on the RTX 2080: tier high now runs on the bf16 bundle ('DiT bf16, q8_fp16 attention'), the 256^2 output
matches the exact path at cosine 0.9986, and 1024^2 works. No speedup: Turing's INT8 MMA m8n8k16 does the same
MACs per instruction as the fp16 m16n8k8 it replaces (m16n8k32 is the 2x part on Ampere), so 8-bit attention is a
bandwidth path here; documented in docs/sm75-quantization.md and docs/sm75.md.
@CloverIris CloverIris changed the title sm75: the VAE encoder and runtime LoRAs run on Turing sm75: VAE encoder, runtime LoRAs, quantized tiers and a q8_fp16 attention kernel on Turing Nov 8, 2026
xycld added 2 commits November 8, 2026 04:08
- attention_q8: the Turing INT8-QK kernel is built for 75-real only, so Volta (also
  sm75::turing()) no longer selects it; q8 stays unsupported there, as before. The
  Turing path now runs the same operand checks as the sm_80 kernels (k, v, out,
  cu_seqlens, head_dim 128, kv_heads, scale, workspace size and alignment).
- Tier high/turbo on sm_7x: an unset attention stays exact. Turing's q8_fp16 is no
  faster than exact there (PR's own figures), so it runs only when asked for.
- Session runs force LoRA placement merge on sm_7x as Pipeline::run does (one helper,
  force_lora_merge_on_sm7x); before, a session LoRA reached the sm_80-only side
  branches and failed mid-run.
- Merged LoRAs on sm_7x: remerge_lora and rebase_merges convert the rewritten
  device-resident weights back to fp16, instead of dropping the DiT on a strength
  change (which bypassed reset_arena and left bf16 bits in fp16 views on a stack
  switch). A stack merging a module for the first time reloads (its resident bits
  are already fp16).
- VAE encoder on sm_7x: encoder_conv2d goes through conv2d (Turing and Volta
  dispatch, its checks); conv2d_down shares the CUTLASS path's operand checks.
- sm75 convert kernels: vectorized only when rows are aligned for it; the float4
  loop also dropped the last 2-3 columns when cols % 4 >= 2.
- quantize --format int4: the converter metadata names convrot_w4a4 layers as such.
- docs/sm75.md: the attention default, the re-merge behaviour, the Numerics heading,
  one encoder timing.
…quest merges too

rebase_merges on sm_7x reloads only when an added module has no pristine copy (the
memo keeps copies of modules a later stack restored). The Session's plan request goes
through force_lora_merge_on_sm7x like its runs, so the setup before the first run builds
the stack the runs use.
@xycld
xycld merged commit e043813 into mossimage-cpp:main Nov 8, 2026
9 checks passed
@xycld xycld mentioned this pull request Nov 8, 2026
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.

2 participants