- fastvideo-kernel/wan22_vae_decode: the Wan2.2 (TI2V-5B) VAE decoder on sm_100a / sm_103a at 704x1248, built on first use against fastvideo-kernel's CUTLASS submodule. It frees intermediates early and carries 4 latent frames per pass: a 5 s video peaks at 20.3 GiB, down from 94.6 GiB in the kernel's first version, with the same output. - WanVAEConfig.decode_kernel routes AutoencoderKLWan.decode through it where it applies (bf16 with fp32 accumulation, about 7x faster than the fp32 decode on a B300, PSNR 61-63 dB, SSIM 0.998 against it); decode_kernel_parallel splits the decode over the sequence-parallel group (2, 4 or 8 GPUs) and broadcasts the video, with the ranks agreeing on whether every one of them could build the kernel. - PAN2's DiT runs fused Triton kernels for Q/K RMSNorm + RoPE and LayerNorm + modulation (pan2_fusions, set_fused_kernels). - PAN2PipelineConfig turns all three on (vae_decode_kernel, vae_decode_kernel_parallel, dit_fused_kernels). - VideoGenerator copies a decoded RGB video's frames to the host once instead of once per frame. - The PAN2 presets sample at guidance 2.5, the reference launcher's default since 2026-10-23.
199 lines
9.3 KiB
Plaintext
199 lines
9.3 KiB
Plaintext
// norm.cu — RMS norm + SiLU + pad for the Wan2.2 VAE decoder, one warp per position.
|
|
//
|
|
// This replaces the seed's norm_vector<C> / pad_vector pair in kernels.cu with the same
|
|
// call contract (norm_pad(x, gamma, hist, pt, ph, silu)) and the same layout (NDHWC,
|
|
// [T+pt][H+2ph][W][C], history frames copied through, pad rows zeroed).
|
|
//
|
|
// Why it is faster (measured in probe/norm_probe2.cu and norm_probe3.cu, one GB200):
|
|
// - the seed is XU-pipe bound (w08's and w14's ncu: 78% SM busy, 16% DRAM, XU 75%):
|
|
// per element it pays cvt bf16->f32 for the sum, five cvts for the reference's three
|
|
// bf16 roundings, __expf and an IEEE divide. This does
|
|
// * bf16 -> f32 by an integer shift (ALU pipe, exact, no XU op),
|
|
// * one rounding for the whole normalize*sqrt(C)*gain product instead of three,
|
|
// which is also closer to the fp32 reference the decode is graded against,
|
|
// * SiLU as u + u*tanh(u) with u = 0.5*pre-activation (one tanh.approx.f32),
|
|
// * bf16 stores two elements at a time (cvt.rn.bf16x2.f32),
|
|
// i.e. ~1.5 XU ops per element instead of ~11.
|
|
// - rows are blocked so the per-position integer divisions of the seed's flat-pixel loop
|
|
// happen once per row, not once per position (at C=256 a position is only 8 elements).
|
|
// - P positions per warp in flight, the number that measured best per channel count.
|
|
//
|
|
// Numerics: one bf16 rounding for the whole normalize*gain product instead of three, and
|
|
// SiLU in fp32 through tanh.approx.f32. That approximation's documented worst case is an
|
|
// absolute error of about 2^-11 (5e-4) in tanh, which is harmless here because it enters
|
|
// the result as u*2^-11 and the stored value is bf16, so the perturbation stays below the
|
|
// bf16 quantum of u + u*tanh(u) over the range these pre-activations take. Measured, and
|
|
// the place to look if that ever changes (bench.py, f121x1 vs the fp32 reference):
|
|
// seed max 0.1709 (0.34x budget) rms 0.002242 (0.38x budget)
|
|
// this max 0.1992 (0.40x budget) rms 0.002187 (0.37x budget)
|
|
// i.e. the max moves up a little, the rms moves down, both far inside the budget.
|
|
#include <ATen/ATen.h>
|
|
#include <ATen/cuda/CUDAContext.h>
|
|
#include <c10/cuda/CUDAException.h>
|
|
#include <cuda_bf16.h>
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <type_traits>
|
|
|
|
using B = __nv_bfloat16;
|
|
|
|
namespace {
|
|
|
|
__device__ __forceinline__ void split2(uint32_t w, float& lo, float& hi) {
|
|
lo = __uint_as_float(w << 16); // exact bf16 -> f32, ALU pipe
|
|
hi = __uint_as_float(w & 0xffff0000u);
|
|
}
|
|
__device__ __forceinline__ float tanh_approx(float x) {
|
|
float r; asm("tanh.approx.f32 %0, %1;" : "=f"(r) : "f"(x)); return r;
|
|
}
|
|
__device__ __forceinline__ uint32_t pack2(float a, float b) {
|
|
uint32_t r; asm("cvt.rn.bf16x2.f32 %0, %1, %2;" : "=r"(r) : "f"(b), "f"(a)); return r;
|
|
}
|
|
|
|
// MODE 0: RMS norm * sqrt(C) * gain, then SiLU. MODE 1: the same, no SiLU. MODE 2: copy only.
|
|
template <int C, int P, int MODE>
|
|
__global__ void norm_pad_kernel(const B* __restrict__ x, const B* __restrict__ gain,
|
|
const B* __restrict__ hist, B* __restrict__ y,
|
|
int T, int H, int W, int PT, int PH) {
|
|
constexpr int CV = C / 8; // uint4 per position
|
|
constexpr int NP = C / 256; // uint4 per lane per position
|
|
const int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, warps = blockDim.x >> 5;
|
|
const int HP = H + 2 * PH;
|
|
const int64_t rows = (int64_t)(T + PT) * HP;
|
|
const float sqrtC = sqrtf((float)C);
|
|
for (int64_t row = blockIdx.x; row < rows; row += gridDim.x) {
|
|
const int h = (int)(row % HP), t = (int)(row / HP);
|
|
const bool interior = (h >= PH && h < H + PH);
|
|
const bool current = (t >= PT); // a history frame is copied through, never re-normalized
|
|
const int ph = h - PH;
|
|
const uint4* src = nullptr;
|
|
if (interior) {
|
|
if (current) src = (const uint4*)x + ((int64_t)(t - PT) * H + ph) * W * CV;
|
|
else if (hist) src = (const uint4*)hist + ((int64_t)t * H + ph) * W * CV;
|
|
}
|
|
for (int w0 = warp * P; w0 < W; w0 += warps * P) {
|
|
uint4 v[P][NP];
|
|
#pragma unroll
|
|
for (int q = 0; q < P; q++) {
|
|
const int w = w0 + q;
|
|
#pragma unroll
|
|
for (int j = 0; j < NP; j++) {
|
|
v[q][j] = make_uint4(0, 0, 0, 0);
|
|
if (src && w < W) v[q][j] = src[(int64_t)w * CV + lane + j * 32];
|
|
}
|
|
}
|
|
if (MODE < 2 && interior && current) {
|
|
float s[P][2];
|
|
#pragma unroll
|
|
for (int q = 0; q < P; q++) { s[q][0] = 0.f; s[q][1] = 0.f; }
|
|
#pragma unroll
|
|
for (int q = 0; q < P; q++) {
|
|
#pragma unroll
|
|
for (int j = 0; j < NP; j++) {
|
|
float f[8];
|
|
split2(v[q][j].x, f[0], f[1]); split2(v[q][j].y, f[2], f[3]);
|
|
split2(v[q][j].z, f[4], f[5]); split2(v[q][j].w, f[6], f[7]);
|
|
#pragma unroll
|
|
for (int k = 0; k < 8; k += 2) {
|
|
s[q][0] = fmaf(f[k], f[k], s[q][0]);
|
|
s[q][1] = fmaf(f[k + 1], f[k + 1], s[q][1]);
|
|
}
|
|
}
|
|
}
|
|
#pragma unroll
|
|
for (int q = 0; q < P; q++) {
|
|
float t0 = s[q][0] + s[q][1];
|
|
#pragma unroll
|
|
for (int d = 16; d; d >>= 1) t0 += __shfl_xor_sync(0xffffffff, t0, d);
|
|
// 0.5 * sqrt(C) / max(sqrt(sum), 1e-12): one multiply per element, u = 0.5*pre-activation
|
|
const float hs = 0.5f * sqrtC * rsqrtf(fmaxf(t0, 1e-24f));
|
|
#pragma unroll
|
|
for (int j = 0; j < NP; j++) {
|
|
const uint4 gn = ((const uint4*)gain)[lane + j * 32];
|
|
float g[8], xf[8], yf[8];
|
|
split2(gn.x, g[0], g[1]); split2(gn.y, g[2], g[3]);
|
|
split2(gn.z, g[4], g[5]); split2(gn.w, g[6], g[7]);
|
|
split2(v[q][j].x, xf[0], xf[1]); split2(v[q][j].y, xf[2], xf[3]);
|
|
split2(v[q][j].z, xf[4], xf[5]); split2(v[q][j].w, xf[6], xf[7]);
|
|
#pragma unroll
|
|
for (int k = 0; k < 8; k++) {
|
|
const float u = xf[k] * hs * g[k];
|
|
yf[k] = (MODE == 0) ? fmaf(u, tanh_approx(u), u) : (2.f * u);
|
|
}
|
|
v[q][j].x = pack2(yf[0], yf[1]); v[q][j].y = pack2(yf[2], yf[3]);
|
|
v[q][j].z = pack2(yf[4], yf[5]); v[q][j].w = pack2(yf[6], yf[7]);
|
|
}
|
|
}
|
|
}
|
|
#pragma unroll
|
|
for (int q = 0; q < P; q++) {
|
|
const int w = w0 + q;
|
|
if (w < W) {
|
|
uint4* dst = (uint4*)y + row * W * CV + (int64_t)w * CV + lane;
|
|
#pragma unroll
|
|
for (int j = 0; j < NP; j++) dst[j * 32] = v[q][j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy + pad for any C divisible by 8: the calls with no gain (post_quant_conv's input, the temporal convs).
|
|
__global__ void copy_row_kernel(const uint4* __restrict__ x, const uint4* __restrict__ hist,
|
|
uint4* __restrict__ y, int T, int H, int W, int PT, int PH, int CV) {
|
|
const int HP = H + 2 * PH;
|
|
const int64_t rows = (int64_t)(T + PT) * HP;
|
|
const int64_t nrow = (int64_t)W * CV;
|
|
for (int64_t row = blockIdx.x; row < rows; row += gridDim.x) {
|
|
const int h = (int)(row % HP), t = (int)(row / HP);
|
|
const bool interior = (h >= PH && h < H + PH);
|
|
const uint4* src = nullptr;
|
|
if (interior) {
|
|
const int tt = t - PT, ph = h - PH;
|
|
if (tt >= 0) src = x + ((int64_t)tt * H + ph) * nrow;
|
|
else if (hist) src = hist + ((int64_t)t * H + ph) * nrow;
|
|
}
|
|
uint4* dst = y + row * nrow;
|
|
const uint4 zz = make_uint4(0, 0, 0, 0);
|
|
for (int64_t i = threadIdx.x; i < nrow; i += blockDim.x) dst[i] = src ? src[i] : zz;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
at::Tensor norm_pad(const at::Tensor& x, const at::Tensor& gamma, const at::Tensor& hist,
|
|
int pt, int ph, bool silu) {
|
|
const int C = (int)x.size(1), T = (int)x.size(2), H = (int)x.size(3), W = (int)x.size(4);
|
|
auto y = at::empty({1, C, T + pt, H + ph * 2, W},
|
|
x.options().memory_format(at::MemoryFormat::ChannelsLast3d));
|
|
const B* xp = (const B*)x.data_ptr<at::BFloat16>();
|
|
const B* gp = gamma.defined() ? (const B*)gamma.data_ptr<at::BFloat16>() : nullptr;
|
|
const B* hp = hist.defined() ? (const B*)hist.data_ptr<at::BFloat16>() : nullptr;
|
|
B* yp = (B*)y.data_ptr<at::BFloat16>();
|
|
const int64_t rows = (int64_t)(T + pt) * (H + ph * 2);
|
|
auto stream = at::cuda::getCurrentCUDAStream();
|
|
const int blocks = (int)std::min<int64_t>(rows, 1024);
|
|
const int threads = 256;
|
|
auto go = [&](auto tag) {
|
|
constexpr int CC = decltype(tag)::value;
|
|
// P positions in flight per warp: measured best at 4 for C<=512, 2 at C=1024
|
|
constexpr int P = (CC == 1024) ? 2 : 4;
|
|
if (!gp) norm_pad_kernel<CC, P, 2><<<blocks, threads, 0, stream>>>(xp, gp, hp, yp, T, H, W, pt, ph);
|
|
else if (silu) norm_pad_kernel<CC, P, 0><<<blocks, threads, 0, stream>>>(xp, gp, hp, yp, T, H, W, pt, ph);
|
|
else norm_pad_kernel<CC, P, 1><<<blocks, threads, 0, stream>>>(xp, gp, hp, yp, T, H, W, pt, ph);
|
|
};
|
|
const bool fast = (C == 1024 || C == 512 || C == 256);
|
|
TORCH_CHECK(!gp || fast, "Unsupported RMS width ", C);
|
|
if (fast) {
|
|
if (C == 1024) go(std::integral_constant<int, 1024>{});
|
|
else if (C == 512) go(std::integral_constant<int, 512>{});
|
|
else go(std::integral_constant<int, 256>{});
|
|
} else {
|
|
TORCH_CHECK(C % 8 == 0, "norm_pad copy needs C divisible by 8, got ", C);
|
|
copy_row_kernel<<<blocks, threads, 0, stream>>>((const uint4*)xp, (const uint4*)hp, (uint4*)yp, T, H, W,
|
|
pt, ph, C / 8);
|
|
}
|
|
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
return y;
|
|
}
|