diff --git a/CMakeLists.txt b/CMakeLists.txt index 8152df92..86252dc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,11 @@ option(FLASHRT_ENABLE_MELBAND_ROFORMER # Enable explicitly on a Blackwell (sm_120) build. option(FLASHRT_ENABLE_OMNIVOICE "Build OmniVoice TTS RTX SM120 fused kernels in flash_rt_omnivoice" OFF) +# MiniMax-Remover — builds flash_rt_minimax_remover.so with fp16-native +# fused RMS_norm + RMS_SiLU CUDA kernels for the Wan VAE. OFF by default; +# enable with -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON. +option(FLASHRT_ENABLE_MINIMAX_REMOVER + "Build MiniMax-Remover VAE fused fp16 kernels in flash_rt_minimax_remover" OFF) option(FLASHRT_ENABLE_SM120_DEV_KERNELS "Build SM120 probe/tilesweep tuning kernels and public bindings" OFF) # ── Slim build for VLA deployments ── @@ -1007,6 +1012,51 @@ else() message(STATUS "OmniVoice TTS SM120 kernels: DISABLED") endif() +# ── MiniMax-Remover VAE fused kernels (standalone module, independent +# of flash_rt_kernels) ── +# Builds flash_rt_minimax_remover.so with fp16-native fused RMS_norm and +# RMS_norm+SiLU CUDA kernels for the Wan VAE decoder/encoder. The module +# is opt-in and remains separate from the default flash_rt_kernels target. +# Enable explicitly on a Blackwell (sm_120) build: +# cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ... +if(FLASHRT_ENABLE_MINIMAX_REMOVER) + pybind11_add_module(flash_rt_minimax_remover + csrc/minimax_remover_extra_bindings.cpp + csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cu + csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cu + csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cu + csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cu + csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cu + csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cu + csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cu + csrc/kernels/minimax_remover/fp16_bias_gate_residual.cu + csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cu + csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cu + csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cu + csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cu + csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cu) + set_target_properties(flash_rt_minimax_remover PROPERTIES + CUDA_STANDARD 17 + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/flash_rt + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/flash_rt) + target_include_directories(flash_rt_minimax_remover PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/csrc + ${CMAKE_CURRENT_SOURCE_DIR}/csrc/kernels) + target_compile_options(flash_rt_minimax_remover PRIVATE + $<$: + --expt-relaxed-constexpr -O3 --use_fast_math + -U__CUDA_NO_HALF_OPERATORS__ + -U__CUDA_NO_HALF2_OPERATORS__ + -U__CUDA_NO_HALF_CONVERSIONS__ + ${GPU_GENCODE} + >) + target_link_libraries(flash_rt_minimax_remover PRIVATE CUDA::cudart CUDA::cublas CUDA::cublasLt) + install(TARGETS flash_rt_minimax_remover LIBRARY DESTINATION flash_rt) + message(STATUS "MiniMax-Remover VAE fused kernels: ENABLED") +else() + message(STATUS "MiniMax-Remover VAE fused kernels: DISABLED") +endif() + if(FLASHRT_BUILD_QWEN3_VL) # SM120 (RTX 5090): NVFP4/FP8 ViT helpers. SM89 (Ada): native FP8 block-128 # GEMM/GEMV + fused act/norm-quant + QK norm-rope kernels for the official diff --git a/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cu b/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cu new file mode 100644 index 00000000..08763319 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cu @@ -0,0 +1,206 @@ +// ================================================================ +// flash_rt_minimax_remover — fused adaLN + FP8 quantise CUDA kernel. +// See fp16_ada_layernorm_quant_fp8.cuh for semantics. +// +// Implementation notes: +// * One CUDA block per row (S rows total). Each row's D elements +// are processed cooperatively via a fp32 reduction. +// * fp16x8 (uint4) vector loads for x; fp16x2 (__half2 loads); +// one warp shuffles + a single-warp reduction across warps via +// shared memory (no atomic in the hot path). +// * scale/shift/x_norm arithmetic done in fp32 to match the +// reference FP32LayerNorm path bit-for-bit within fp16 tolerance. +// * Output is packed as fp8x4 uint32 stores. +// +// D must be a multiple of 8 (all MiniMax-Remover Linears satisfy this: +// D ∈ {1536}). Kernel assumes D <= 4096 * threads * 8 which covers +// every practical value; caller sizes the block accordingly. +// ================================================================ +#include "fp16_ada_layernorm_quant_fp8.cuh" + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int VEC = 8; // 8 fp16 per uint4 load + +__device__ __forceinline__ float warp_reduce_sum(float v) { + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + v += __shfl_xor_sync(0xffffffff, v, off); + return v; +} + +// One block per row; blockDim.x = THREADS (128 = 4 warps). Each thread +// walks the row in VEC-strided chunks: thread t handles elements +// t*VEC, (t + THREADS)*VEC, ... D is a multiple of VEC. +template +__global__ void ada_layernorm_quant_fp8_kernel( + const __half* __restrict__ x_in, + const float* __restrict__ scale_vec, + const float* __restrict__ shift_vec, + const float* __restrict__ act_scale_ptr, + __nv_fp8_e4m3* __restrict__ out, + int S, int D, float eps) +{ + const int s = blockIdx.x; + const int tid = threadIdx.x; + const int lane = tid & 31; + const int warp = tid >> 5; + constexpr int NWARPS = THREADS / 32; + + const __half* xp = x_in + s * D; + __nv_fp8_e4m3* op = out + s * D; + + const int D_vec = D / VEC; // number of uint4 chunks + // Pass 1 + 2 fused: single-load streaming stats (Welford-style, but + // we prefer a simple two-pass with L2 reuse since D is small enough + // that x is fully resident after first pass). Two passes are used + // here to keep the code short and match the reference fp32 + // LayerNorm bit-for-bit. + + // ── Pass 1: sum ────────────────────────────────────────── + float local_sum = 0.f; + for (int v = tid; v < D_vec; v += THREADS) { + const uint4* p = reinterpret_cast(xp + v * VEC); + uint4 raw = *p; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + local_sum += f0.x + f0.y + f1.x + f1.y + + f2.x + f2.y + f3.x + f3.y; + } + // Warp reduce, then cross-warp via smem. + __shared__ float smem_sum[NWARPS]; + float wsum = warp_reduce_sum(local_sum); + if (lane == 0) smem_sum[warp] = wsum; + __syncthreads(); + float mean; + if (warp == 0) { + float v = (lane < NWARPS) ? smem_sum[lane] : 0.f; + v = warp_reduce_sum(v); + if (lane == 0) smem_sum[0] = v / (float)D; + } + __syncthreads(); + mean = smem_sum[0]; + + // ── Pass 2: sum of squared deviations ───────────────────── + float local_sq = 0.f; + for (int v = tid; v < D_vec; v += THREADS) { + const uint4* p = reinterpret_cast(xp + v * VEC); + uint4 raw = *p; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + float d0 = f0.x - mean, d1 = f0.y - mean; + float d2 = f1.x - mean, d3 = f1.y - mean; + float d4 = f2.x - mean, d5 = f2.y - mean; + float d6 = f3.x - mean, d7 = f3.y - mean; + local_sq += d0*d0 + d1*d1 + d2*d2 + d3*d3 + + d4*d4 + d5*d5 + d6*d6 + d7*d7; + } + __shared__ float smem_sq[NWARPS]; + float wsq = warp_reduce_sum(local_sq); + if (lane == 0) smem_sq[warp] = wsq; + __syncthreads(); + float rstd; + if (warp == 0) { + float v = (lane < NWARPS) ? smem_sq[lane] : 0.f; + v = warp_reduce_sum(v); + if (lane == 0) smem_sq[0] = rsqrtf(v / (float)D + eps); + } + __syncthreads(); + rstd = smem_sq[0]; + + // ── Pass 3: normalise + adaLN modulate + fp8 quantise ──── + const float act_scale = *act_scale_ptr; + const float inv_a = 1.0f / fmaxf(act_scale, 1e-12f); + for (int v = tid; v < D_vec; v += THREADS) { + const int d0 = v * VEC; + const uint4* px = reinterpret_cast(xp + d0); + uint4 raw = *px; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + float xv[VEC]; + { + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + xv[0]=f0.x; xv[1]=f0.y; xv[2]=f1.x; xv[3]=f1.y; + xv[4]=f2.x; xv[5]=f2.y; xv[6]=f3.x; xv[7]=f3.y; + } + // scale/shift are fp32, read sequentially. + float sv[VEC], bv[VEC]; + // 8 fp32 loads via 2×float4: + const float4* pS = reinterpret_cast(scale_vec + d0); + const float4* pB = reinterpret_cast(shift_vec + d0); + float4 s0 = pS[0], s1 = pS[1]; + float4 b0 = pB[0], b1 = pB[1]; + sv[0]=s0.x; sv[1]=s0.y; sv[2]=s0.z; sv[3]=s0.w; + sv[4]=s1.x; sv[5]=s1.y; sv[6]=s1.z; sv[7]=s1.w; + bv[0]=b0.x; bv[1]=b0.y; bv[2]=b0.z; bv[3]=b0.w; + bv[4]=b1.x; bv[5]=b1.y; bv[6]=b1.z; bv[7]=b1.w; + + __nv_fp8_e4m3 fp8_pack[VEC]; + #pragma unroll + for (int i = 0; i < VEC; i++) { + float xn = (xv[i] - mean) * rstd; + float y = xn * (1.0f + sv[i]) + bv[i]; + float yq = y * inv_a; + yq = fminf(fmaxf(yq, -448.0f), 448.0f); + fp8_pack[i] = __nv_fp8_e4m3(yq); + } + // Pack 8 fp8 into 2×uint32 stores. + uint32_t* out_u32 = reinterpret_cast(op + d0); + out_u32[0] = *reinterpret_cast(&fp8_pack[0]); + out_u32[1] = *reinterpret_cast(&fp8_pack[4]); + } +} + +} // anonymous namespace + +int fp16_ada_layernorm_quant_fp8( + const void* x_fp16, + const void* scale_fp32, + const void* shift_fp32, + const void* act_scale_fp32, + void* out_fp8, + int S, int D, float eps, + cudaStream_t stream) +{ + if (!x_fp16 || !scale_fp32 || !shift_fp32 || !act_scale_fp32 || !out_fp8) + return -1; + if (S <= 0 || D <= 0 || (D % VEC) != 0) return -2; + constexpr int THREADS = 128; + ada_layernorm_quant_fp8_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(scale_fp32), + reinterpret_cast(shift_fp32), + reinterpret_cast(act_scale_fp32), + reinterpret_cast<__nv_fp8_e4m3*>(out_fp8), + S, D, eps); + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cuh b/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cuh new file mode 100644 index 00000000..e609de3c --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cuh @@ -0,0 +1,55 @@ +// ================================================================ +// flash_rt_minimax_remover — fused adaLN + FP8 quantise kernel. +// +// Single-kernel fusion of the FP8 attention/FFN entry path: +// (1) FP32-statistics LayerNorm across D +// mean = mean(x[s,:]) // fp32 reduce +// var = mean((x - mean)^2) // fp32 reduce +// rstd = 1 / sqrt(var + eps) +// (2) adaLN modulation (fp32 scale/shift from temb.float()): +// y = (x - mean) * rstd * (1 + scale[d]) + shift[d] +// (3) Per-tensor FP8 e4m3 quantise (static act_scale from the FP8 +// Linear that will consume this output): +// y_fp8 = clip(y / act_scale, ±448) cast to fp8_e4m3fn +// +// Replaces the 3-kernel path in the FP8 transformer block entry: +// ada_layernorm_fp16_io → quantize_fp8_static_fp16 → fp8_gemm +// Eliminates one full [S,D] fp16 read-modify-write on the LayerNorm +// output. The output tensor is the pre-quantised input of the next +// FP8 Linear, so its .forward_from_fp8() (or gemm_from_fp8_ext for +// Q/K/V shared-scale) can skip its own activation quantise entirely. +// +// Layout: +// x : [S, D] fp16, contiguous row-major +// scale : [D] fp32 (from temb.float().chunk(6)) +// shift : [D] fp32 +// act_scale: [1] fp32 device scalar (target Linear's descale factor) +// out : [S, D] fp8_e4m3fn contiguous row-major +// +// Grid: one CUDA block per row. Each block reduces over D in fp32. +// ================================================================ +#ifndef FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_ADA_LN_QUANT_FP8_CUH +#define FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_ADA_LN_QUANT_FP8_CUH + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused fp32-stat LayerNorm + adaLN modulation + per-tensor fp8 quantise. +// Returns 0 on success, negative on invalid args. +int fp16_ada_layernorm_quant_fp8( + const void* x_fp16, + const void* scale_fp32, + const void* shift_fp32, + const void* act_scale_fp32, + void* out_fp8, + int S, int D, float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt + +#endif // FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_ADA_LN_QUANT_FP8_CUH diff --git a/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cu b/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cu new file mode 100644 index 00000000..0c849330 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cu @@ -0,0 +1,146 @@ +// ================================================================ +// flash_rt_minimax_remover — fused bias + gate·residual kernel. +// See fp16_bias_gate_residual.cuh for the semantics. +// +// Optimisation summary vs the stock `add_bias_fp16` + Triton +// `gate_mul_residual_bcast` sequence: +// * Removes the intermediate fp16 read-modify-write on `out` +// (one full [M,D] pass eliminated per call). +// * fp16x8 (uint4) vector loads/stores → 1/8 the memory +// transactions of the scalar bias kernel. +// * Bias & gate rows are cooperatively staged to shared memory +// once per block (each block covers `THREADS * VEC` = 8×D0 +// columns of a single row cache-line width). +// +// Layout constraints: +// D % 8 == 0 (MiniMax-Remover Linears: 1536, 8960 — both /8). +// ================================================================ +#include "fp16_bias_gate_residual.cuh" + +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +// One thread processes VEC=8 fp16 elements via one uint4 (=8×fp16) op. +constexpr int VEC = 8; + +__device__ __forceinline__ void load_vec8(const __half* p, __half2 out[4]) { + const uint4* pu = reinterpret_cast(p); + uint4 v = *pu; + out[0] = *reinterpret_cast<__half2*>(&v.x); + out[1] = *reinterpret_cast<__half2*>(&v.y); + out[2] = *reinterpret_cast<__half2*>(&v.z); + out[3] = *reinterpret_cast<__half2*>(&v.w); +} + +__device__ __forceinline__ void store_vec8(__half* p, const __half2 x[4]) { + uint4 v; + v.x = *reinterpret_cast(&x[0]); + v.y = *reinterpret_cast(&x[1]); + v.z = *reinterpret_cast(&x[2]); + v.w = *reinterpret_cast(&x[3]); + *reinterpret_cast(p) = v; +} + +// grid.x = M, grid.y = D_vec = D/8 +// each thread → one 8-element column tile of one row. +__global__ void bias_gate_residual_kernel( + const __half* __restrict__ out, + const __half* __restrict__ bias, + const __half* __restrict__ gate, + __half* __restrict__ residual, + int M, int D_vec) { + const int m = blockIdx.x; + const int dv = blockIdx.y * blockDim.x + threadIdx.x; + if (dv >= D_vec) return; + + const int d = dv * VEC; + const int base = m * (D_vec * VEC) + d; + + __half2 o[4], b[4], g[4], r[4]; + load_vec8(out + base, o); + load_vec8(bias + d, b); + load_vec8(gate + d, g); + load_vec8(residual + base, r); + + #pragma unroll + for (int i = 0; i < 4; i++) { + // (o + b) * g — in fp16 for speed. Bias values are small + // (≪ fp16 max) and the residual is already fp16, so no + // range issues; matches the numerical behaviour of the + // previous (scalar bias fp16 + Triton fp32-accum gate) + // pipeline within fp16 rounding tolerance. + __half2 sum = __hadd2(o[i], b[i]); + __half2 mul = __hmul2(sum, g[i]); + r[i] = __hadd2(r[i], mul); + } + store_vec8(residual + base, r); +} + +__global__ void add_bias_vec8_kernel( + __half* __restrict__ x, + const __half* __restrict__ bias, + int M, int D_vec) { + const int m = blockIdx.x; + const int dv = blockIdx.y * blockDim.x + threadIdx.x; + if (dv >= D_vec) return; + + const int d = dv * VEC; + const int base = m * (D_vec * VEC) + d; + + __half2 xv[4], bv[4]; + load_vec8(x + base, xv); + load_vec8(bias + d, bv); + #pragma unroll + for (int i = 0; i < 4; i++) xv[i] = __hadd2(xv[i], bv[i]); + store_vec8(x + base, xv); +} + +} // namespace + +int fp16_bias_gate_residual_bcast( + const void* out_fp16, + const void* bias_fp16, + const void* gate_fp16, + void* residual_fp16, + int M, int D, + cudaStream_t stream) { + if (!out_fp16 || !bias_fp16 || !gate_fp16 || !residual_fp16) return -1; + if (M <= 0 || D <= 0 || (D % VEC) != 0) return -2; + const int D_vec = D / VEC; + const int threads = 128; + dim3 grid(M, (D_vec + threads - 1) / threads); + bias_gate_residual_kernel<<>>( + reinterpret_cast(out_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast(gate_fp16), + reinterpret_cast<__half*>(residual_fp16), + M, D_vec); + return 0; +} + +int fp16_add_bias_vec8( + void* x_fp16, + const void* bias_fp16, + int M, int D, + cudaStream_t stream) { + if (!x_fp16 || !bias_fp16) return -1; + if (M <= 0 || D <= 0 || (D % VEC) != 0) return -2; + const int D_vec = D / VEC; + const int threads = 128; + dim3 grid(M, (D_vec + threads - 1) / threads); + add_bias_vec8_kernel<<>>( + reinterpret_cast<__half*>(x_fp16), + reinterpret_cast(bias_fp16), + M, D_vec); + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cuh b/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cuh new file mode 100644 index 00000000..13e35c34 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_bias_gate_residual.cuh @@ -0,0 +1,67 @@ +// ================================================================ +// flash_rt_minimax_remover — fused bias + gate·residual kernel. +// +// The MiniMax-Remover transformer block runs the following pattern +// after every attention output (O-proj) and every FFN down proj: +// +// out_fp16 = FP8_GEMM(...) // no bias yet +// add_bias_fp16(out, bias, S, D) // RMW on out +// gate_mul_residual_bcast(residual, out, gate) // residual += out*gate +// +// The middle step is a full fp16 read-modify-write pass over out that +// happens JUST BEFORE the residual add reads out again — cache-hostile +// (2 * S * D fp16 traffic per pass, ~100 MB per call at S=32256/D=1536). +// +// This kernel folds them into one: +// residual[i] += (out[i] + bias[i % D]) * gate[i % D] +// which drops the intermediate RMW on `out` entirely (one fewer full +// pass over ~50M fp16 elements per call). Node-level profiling put +// `add_bias_fp16` at 280 ms / 1812 calls; the O-proj + FFN-down slots +// account for 720 of those calls — this kernel eliminates them. +// +// Layout / semantics: +// out : [M, D] fp16, row-major (read-only) +// bias : [D] fp16 (broadcast along M) +// gate : [D] fp16 (broadcast along M) +// residual : [M, D] fp16 (accumulated in place) +// +// Uses fp16x8 vector loads/stores (uint4) so each thread processes 8 +// elements, cutting global memory transactions 8× versus the scalar +// `add_bias_fp16` kernel. D is assumed to be a multiple of 8 (true +// for all MiniMax-Remover Linears; D ∈ {1536, 8960}). +// ================================================================ +#ifndef FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GATE_RES_CUH +#define FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GATE_RES_CUH + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused: residual[m,d] += (out[m,d] + bias[d]) * gate[d] +// +// All buffers are fp16. bias and gate are broadcast vectors of length D. +// Returns 0 on success, negative on invalid args. +int fp16_bias_gate_residual_bcast( + const void* out_fp16, + const void* bias_fp16, + const void* gate_fp16, + void* residual_fp16, + int M, int D, + cudaStream_t stream); + +// Vectorised replacement for the generic scalar add_bias_fp16: +// x[m,d] = x[m,d] + bias[d] (broadcast bias, in-place) +// Uses fp16x8 (uint4) accesses; D must be a multiple of 8. +int fp16_add_bias_vec8( + void* x_fp16, + const void* bias_fp16, + int M, int D, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt + +#endif // FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GATE_RES_CUH diff --git a/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cu b/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cu new file mode 100644 index 00000000..fd771992 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cu @@ -0,0 +1,170 @@ +// ================================================================ +// flash_rt_minimax_remover — fused FFN epilogue kernels. +// +// Replaces the 3-kernel sequence in the MiniMax-Remover transformer +// FFN path: +// add_bias_fp16 (read fp16 [M,N] + write fp16 [M,N]) +// gelu_inplace (read fp16 [M,N] + write fp16 [M,N]) +// quantize_fp8 (read fp16 [M,N] + write fp8 [M,N]) +// with a single pass that reads the GEMM's raw fp16 output ONCE and +// writes fp8 ONCE — eliminating ~3 full-tensor memory round-trips. +// +// The output fp8 tensor is the pre-quantised input of the NEXT FP8 +// Linear (net.2), which skips its own activation quantise step. +// +// Precision: all arithmetic (bias add + tanh-gelu) is done in fp32 +// before the fp8 cast, so the result is actually more accurate than +// the original path (which rounds to fp16 twice along the way). +// ================================================================ +#include "fp16_bias_gelu_quant_fp8.cuh" + +#include +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +__device__ __forceinline__ float gelu_tanh(float x) { + // Matches gelu_inplace_fp16 / gelu_tanh_nvfp4 exactly: + // 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))) + constexpr float kSqrt2Pi = 0.7978845608f; + float t = tanhf(kSqrt2Pi * (x + 0.044715f * x * x * x)); + return x * 0.5f * (1.0f + t); +} + +// ── Fused: bias + tanh-gelu + quantise → fp8 e4m3 ── +// 4 elements / thread, vectorised fp16 reads, packed fp8 writes. +// Requires N % 4 == 0 (all MiniMax-Remover Linears satisfy this: +// inner_dim=13824, dim=5120, etc.). +__global__ void bias_gelu_quant_fp16_fp8_kernel( + const __half* __restrict__ gemm_out, // [M*N] raw GEMM output (no bias) + const __half* __restrict__ bias, // [N] + __nv_fp8_e4m3* __restrict__ out, // [M*N] + const float* __restrict__ d_scale, // act_scale of the NEXT linear + int M, int N) +{ + const int n_total = M * N; + int i = (blockIdx.x * blockDim.x + threadIdx.x) * 4; + if (i >= n_total) return; + + const float inv_scale = 1.0f / fmaxf(*d_scale, 1e-12f); + const int col = i % N; // N%4==0 ⇒ col is a multiple of 4, no row-cross + + // Load 4 fp16 GEMM-output values. + const __half2* in2 = reinterpret_cast(gemm_out + i); + __half2 vA = in2[0]; + __half2 vB = in2[1]; + float fv[4] = { + __half2float(vA.x), __half2float(vA.y), + __half2float(vB.x), __half2float(vB.y) + }; + + // Load 4 fp16 bias values (sequential within the row). + const __half2* bias2 = reinterpret_cast(bias + col); + __half2 bA = bias2[0]; + __half2 bB = bias2[1]; + float bv[4] = { + __half2float(bA.x), __half2float(bA.y), + __half2float(bB.x), __half2float(bB.y) + }; + + // bias + gelu(tanh) + quantise, all in fp32. + __nv_fp8_e4m3 fp8_pack[4]; + #pragma unroll + for (int j = 0; j < 4; j++) { + float v = fv[j] + bv[j]; + float g = gelu_tanh(v); + g = fminf(fmaxf(g * inv_scale, -448.0f), 448.0f); + fp8_pack[j] = __nv_fp8_e4m3(g); + } + *reinterpret_cast(out + i) = + *reinterpret_cast(fp8_pack); +} + +// ── Fused: bias + identity + quantise → fp8 e4m3 ── +// Used for Linear→Linear chains with NO activation in between +// (kept for completeness / future attention QKV fusion). +__global__ void bias_quant_fp16_fp8_kernel( + const __half* __restrict__ gemm_out, + const __half* __restrict__ bias, + __nv_fp8_e4m3* __restrict__ out, + const float* __restrict__ d_scale, + int M, int N) +{ + const int n_total = M * N; + int i = (blockIdx.x * blockDim.x + threadIdx.x) * 4; + if (i >= n_total) return; + + const float inv_scale = 1.0f / fmaxf(*d_scale, 1e-12f); + const int col = i % N; + + const __half2* in2 = reinterpret_cast(gemm_out + i); + __half2 vA = in2[0]; + __half2 vB = in2[1]; + + const __half2* bias2 = reinterpret_cast(bias + col); + __half2 bA = bias2[0]; + __half2 bB = bias2[1]; + + __nv_fp8_e4m3 fp8_pack[4]; + #pragma unroll + for (int j = 0; j < 4; j++) { + float v = (j == 0 ? __half2float(vA.x) : + j == 1 ? __half2float(vA.y) : + j == 2 ? __half2float(vB.x) : + __half2float(vB.y)) + + (j == 0 ? __half2float(bA.x) : + j == 1 ? __half2float(bA.y) : + j == 2 ? __half2float(bB.x) : + __half2float(bB.y)); + v = fminf(fmaxf(v * inv_scale, -448.0f), 448.0f); + fp8_pack[j] = __nv_fp8_e4m3(v); + } + *reinterpret_cast(out + i) = + *reinterpret_cast(fp8_pack); +} + +} // anonymous namespace + +int bias_gelu_quant_fp16_fp8( + const void* gemm_out, const void* bias, + void* out, const float* d_scale, + int M, int N, cudaStream_t stream) +{ + if (M <= 0 || N <= 0 || (N & 3) != 0) return -1; + const int n_total = M * N; + const int threads = 256; + const int blocks = (n_total / 4 + threads - 1) / threads; + bias_gelu_quant_fp16_fp8_kernel<<>>( + reinterpret_cast(gemm_out), + reinterpret_cast(bias), + reinterpret_cast<__nv_fp8_e4m3*>(out), + d_scale, M, N); + return 0; +} + +int bias_quant_fp16_fp8( + const void* gemm_out, const void* bias, + void* out, const float* d_scale, + int M, int N, cudaStream_t stream) +{ + if (M <= 0 || N <= 0 || (N & 3) != 0) return -1; + const int n_total = M * N; + const int threads = 256; + const int blocks = (n_total / 4 + threads - 1) / threads; + bias_quant_fp16_fp8_kernel<<>>( + reinterpret_cast(gemm_out), + reinterpret_cast(bias), + reinterpret_cast<__nv_fp8_e4m3*>(out), + d_scale, M, N); + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cuh b/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cuh new file mode 100644 index 00000000..3f2eebf5 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cuh @@ -0,0 +1,40 @@ +// ================================================================ +// flash_rt_minimax_remover — fused FFN epilogue kernel declarations. +// +// bias_gelu_quant_fp16_fp8 : fp16 GEMM-out + bias → tanh-gelu → fp8 +// bias_quant_fp16_fp8 : fp16 GEMM-out + bias → fp8 (identity) +// +// Both eliminate the intermediate fp16 round-trips of the separate +// add_bias_fp16 + (gelu_inplace) + quantize_fp8 sequence. +// ================================================================ +#ifndef FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GELU_QUANT_FP8_CUH +#define FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GELU_QUANT_FP8_CUH + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused: bias-add + tanh-gelu + quantise → fp8 e4m3. +// gemm_out : [M*N] fp16, raw FP8-GEMM output (no bias yet) +// bias : [N] fp16 +// out : [M*N] fp8 e4m3 (the pre-quantised input for the next Linear) +// d_scale : float, the NEXT linear's act_scale (quantise divides by it) +// Returns 0 on success, <0 on invalid args. +int bias_gelu_quant_fp16_fp8( + const void* gemm_out, const void* bias, + void* out, const float* d_scale, + int M, int N, cudaStream_t stream); + +// Fused: bias-add + quantise → fp8 e4m3 (identity activation). +int bias_quant_fp16_fp8( + const void* gemm_out, const void* bias, + void* out, const float* d_scale, + int M, int N, cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt + +#endif // FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_BIAS_GELU_QUANT_FP8_CUH diff --git a/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cu b/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cu new file mode 100644 index 00000000..505bc8e0 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cu @@ -0,0 +1,203 @@ +// ================================================================ +// flash_rt — MiniMax-Remover FP8 per-tensor activation quantize. +// +// 2-pass fused launcher (no host sync): +// Pass 1: grid-stride amax reduction → atomicMax into amax_buf. +// Pass 2: read amax_buf on device, compute scale, grid-stride +// quantize fp16 → fp8 e4m3. Writes scale to scale_out. +// ================================================================ + +#include "fp16_quant_fp8_per_tensor.cuh" +#include +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int Q_THREADS = 256; +constexpr int Q_BLOCKS = 256; // cap grid for amax pass + +__global__ void amax_fp16_kernel( + const __half* __restrict__ x, int n, float* amax_out) +{ + int tid = threadIdx.x; + int idx = blockIdx.x * Q_THREADS + tid; + int stride = Q_THREADS * gridDim.x; + float local = 0.0f; + for (int i = idx; i < n; i += stride) { + local = fmaxf(local, fabsf(__half2float(x[i]))); + } + // Warp reduce + for (int d = 16; d > 0; d >>= 1) + local = fmaxf(local, __shfl_xor_sync(0xffffffff, local, d)); + // Block reduce via shared memory + __shared__ float smem[8]; // Q_THREADS / 32 = 8 warps + int warp_id = tid / 32; + int lane = tid % 32; + if (lane == 0) smem[warp_id] = local; + __syncthreads(); + if (warp_id == 0) { + local = (lane < 8) ? smem[lane] : 0.0f; + for (int d = 4; d > 0; d >>= 1) + local = fmaxf(local, __shfl_xor_sync(0xffffffff, local, d)); + if (lane == 0) { + // atomicMax on reinterpreted int works for non-negative floats + // (IEEE 754 preserves ordering for sign bit = 0). + atomicMax(reinterpret_cast(amax_out), __float_as_int(local)); + } + } +} + +__global__ void quantize_fp16_to_fp8_kernel( + const __half* __restrict__ x, + __nv_fp8_e4m3* __restrict__ y, + int n, const float* amax_in, float* scale_out) +{ + __shared__ float s_inv_scale; + if (threadIdx.x == 0) { + float amax = fmaxf(*amax_in, 0.0f); + float scale = amax * (1.0f / 448.0f); + if (scale < 1e-6f) scale = 1e-6f; + if (scale_out != nullptr) *scale_out = scale; + s_inv_scale = 1.0f / scale; + } + __syncthreads(); + float inv_scale = s_inv_scale; + + int idx = blockIdx.x * Q_THREADS + threadIdx.x; + int stride = Q_THREADS * gridDim.x; + for (int i = idx; i < n; i += stride) { + float val = __half2float(x[i]) * inv_scale; + val = fminf(fmaxf(val, -448.0f), 448.0f); + y[i] = __nv_fp8_e4m3(val); + } +} + +} // anonymous namespace + +int fp16_quant_fp8_per_tensor( + const void* x_fp16, void* y_fp8, + void* scale_out, void* amax_buf, + int n, cudaStream_t stream) +{ + if (n <= 0) return -1; + cudaError_t e; + e = cudaMemsetAsync(amax_buf, 0, sizeof(float), stream); + if (e != cudaSuccess) return -2; + + int blocks_amax = (n + Q_THREADS - 1) / Q_THREADS; + if (blocks_amax > Q_BLOCKS) blocks_amax = Q_BLOCKS; + + amax_fp16_kernel<<>>( + reinterpret_cast(x_fp16), n, + reinterpret_cast(amax_buf)); + + int blocks_q = (n + Q_THREADS - 1) / Q_THREADS; + quantize_fp16_to_fp8_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast<__nv_fp8_e4m3*>(y_fp8), n, + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out)); + + e = cudaGetLastError(); + if (e != cudaSuccess) return -3; + return 0; +} + +// ── Standalone amax (for multi-tensor shared-scale quantization) ── +int amax_fp16( + const void* x_fp16, void* amax_buf, + int n, cudaStream_t stream) +{ + if (n <= 0) return -1; + int blocks = (n + Q_THREADS - 1) / Q_THREADS; + if (blocks > Q_BLOCKS) blocks = Q_BLOCKS; + amax_fp16_kernel<<>>( + reinterpret_cast(x_fp16), n, + reinterpret_cast(amax_buf)); + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -2; +} + +// ── Standalone quantize (reads pre-computed amax from device) ── +int quantize_fp16_fp8_with_amax( + const void* x_fp16, void* y_fp8, + const void* amax_buf, void* scale_out, + int n, cudaStream_t stream) +{ + if (n <= 0) return -1; + int blocks = (n + Q_THREADS - 1) / Q_THREADS; + quantize_fp16_to_fp8_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast<__nv_fp8_e4m3*>(y_fp8), n, + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out)); + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -2; +} + +// ── Dual quantize: two buffers, one shared amax, one launch ── +__global__ void quantize_fp16_to_fp8_dual_kernel( + const __half* __restrict__ x1, + __nv_fp8_e4m3* __restrict__ y1, int n1, + const __half* __restrict__ x2, + __nv_fp8_e4m3* __restrict__ y2, int n2, + const float* amax_in, float* scale_out) +{ + __shared__ float s_inv_scale; + if (threadIdx.x == 0) { + float amax = fmaxf(*amax_in, 0.0f); + float scale = amax * (1.0f / 448.0f); + if (scale < 1e-6f) scale = 1e-6f; + if (scale_out != nullptr) *scale_out = scale; + s_inv_scale = 1.0f / scale; + } + __syncthreads(); + float inv_scale = s_inv_scale; + + // Grid-stride over both buffers using a unified index space + int total = n1 + n2; + int idx = blockIdx.x * Q_THREADS + threadIdx.x; + int stride = Q_THREADS * gridDim.x; + for (int i = idx; i < total; i += stride) { + if (i < n1) { + float val = __half2float(x1[i]) * inv_scale; + val = fminf(fmaxf(val, -448.0f), 448.0f); + y1[i] = __nv_fp8_e4m3(val); + } else { + int j = i - n1; + float val = __half2float(x2[j]) * inv_scale; + val = fminf(fmaxf(val, -448.0f), 448.0f); + y2[j] = __nv_fp8_e4m3(val); + } + } +} + +int quantize_fp16_fp8_with_amax_dual( + const void* x1_fp16, void* y1_fp8, int n1, + const void* x2_fp16, void* y2_fp8, int n2, + const void* amax_buf, void* scale_out, + cudaStream_t stream) +{ + if (n1 <= 0 && n2 <= 0) return -1; + int total = n1 + n2; + int blocks = (total + Q_THREADS - 1) / Q_THREADS; + quantize_fp16_to_fp8_dual_kernel<<>>( + reinterpret_cast(x1_fp16), + reinterpret_cast<__nv_fp8_e4m3*>(y1_fp8), n1, + reinterpret_cast(x2_fp16), + reinterpret_cast<__nv_fp8_e4m3*>(y2_fp8), n2, + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out)); + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -2; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cuh b/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cuh new file mode 100644 index 00000000..970d5f20 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_quant_fp8_per_tensor.cuh @@ -0,0 +1,54 @@ +// ================================================================ +// flash_rt — MiniMax-Remover FP8 per-tensor activation quantize. +// +// Fused 2-pass launcher: +// Pass 1: parallel amax reduction (block-local → atomicMax). +// Pass 2: read device-resident amax, compute scale, quantize. +// +// No host sync — scale stays on device. Works on any contiguous +// fp16 buffer (the caller passes a channels-last 3D tensor whose +// physical memory is NDHWC, and the fp8 output preserves that order). +// ================================================================ +#pragma once +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Quantize x_fp16 [n elements] → y_fp8, writing per-tensor scale to +// scale_out (device float). amax_buf is a scratch device float +// (1 element) used for the inter-pass reduction. +// +// Returns 0 on success, negative on error. +int fp16_quant_fp8_per_tensor( + const void* x_fp16, void* y_fp8, + void* scale_out, void* amax_buf, + int n, cudaStream_t stream); + +// amax only: grid-stride reduction with atomicMax into amax_buf. +// Caller MUST zero amax_buf before the first call. Multiple calls +// accumulate (so amax over two tensors = call on each sequentially). +int amax_fp16( + const void* x_fp16, void* amax_buf, + int n, cudaStream_t stream); + +// Quantize only: reads pre-computed amax from amax_buf, computes +// scale = max(amax,0)/448, writes scale to scale_out, quantizes. +int quantize_fp16_fp8_with_amax( + const void* x_fp16, void* y_fp8, + const void* amax_buf, void* scale_out, + int n, cudaStream_t stream); + +// Dual quantize: quantizes TWO fp16 buffers with the SAME shared +// amax in a single kernel launch. Saves one launch vs calling +// quantize_fp16_fp8_with_amax twice. Writes one shared scale. +int quantize_fp16_fp8_with_amax_dual( + const void* x1_fp16, void* y1_fp8, int n1, + const void* x2_fp16, void* y2_fp8, int n2, + const void* amax_buf, void* scale_out, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cu b/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cu new file mode 100644 index 00000000..a5eb85b8 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cu @@ -0,0 +1,447 @@ +// FlashRT — MiniMax-Remover WanVAE NVFP4 fused quantization kernels (sm_120a). +// +// See fp16_quant_nvfp4_ndhwc.cuh for interface docs. +// +// Design adapted from motus_bf16_rms_silu_quant_nvfp4_sm120.cu with: +// - fp16 input (not bf16) — eliminates the fp16→bf16 cast. +// - kThreadsY=6 (not 8) — supports WanVAE channels 96/192/384. +// - SiLU in fp32 (not bf16-rounded) — preserves fp16 mantissa precision. +// - Single-pass quant: norm+silu+block-scale+quant in one walk over xcache, +// no intermediate buffer (lower register pressure than motus variant). + +#include "fp16_quant_nvfp4_ndhwc.cuh" + +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int kThreadsX = 32; +constexpr int kThreadsY = 6; +constexpr int kThreads = kThreadsX * kThreadsY; // 192 +constexpr int kWBlock = kThreadsX; // 32 +constexpr int kPadFp4 = 4; +constexpr int kMaxHalf2 = 64; // covers c_per_y up to 128 + +__device__ __forceinline__ uint8_t fp32_to_e2m1(float v) { + uint8_t sign = (v < 0.0f) ? 0x8u : 0x0u; + float a = fabsf(v); + uint8_t mag; + if (a < 0.25f) mag = 0; + else if (a < 0.75f) mag = 1; + else if (a < 1.25f) mag = 2; + else if (a < 1.75f) mag = 3; + else if (a < 2.5f) mag = 4; + else if (a < 3.5f) mag = 5; + else if (a < 5.0f) mag = 6; + else mag = 7; + return sign | mag; +} + +__device__ __forceinline__ uint8_t fp32_to_ue4m3_ceil(float v) { + if (v <= 0.0f) return 0; + if (v > 240.0f) return 0xFE; + uint32_t bits = __float_as_uint(v); + int float_exp = ((bits >> 23) & 0xFF) - 127; + uint32_t frac = bits & 0x7FFFFF; + int ue_exp = float_exp + 7; + if (ue_exp <= 0) { + float scaled = v * 512.0f; + int m = (int)ceilf(scaled); + if (m > 7) return (1 << 3) | 0; + if (m < 1) m = 1; + return (uint8_t)m; + } + if (ue_exp >= 15) return 0xFE; + int m = (int)(frac >> 20); + if (frac & 0xFFFFF) m++; + if (m >= 8) { m = 0; ue_exp++; } + if (ue_exp >= 15) return 0xFE; + return (uint8_t)((ue_exp << 3) | m); +} + +__device__ __forceinline__ float ue4m3_to_fp32(uint8_t v) { + int e = (v >> 3) & 0xF; + int m = v & 0x7; + if (e == 0) return ldexpf((float)m / 8.0f, -6); + return ldexpf(1.0f + (float)m / 8.0f, e - 7); +} + +__device__ __forceinline__ float silu_f32(float x) { + return x * (1.0f / (1.0f + __expf(-x))); +} + +template +__global__ void quant_nvfp4_kernel( + const __half* __restrict__ x, + const __half* __restrict__ gamma, + const __half* __restrict__ bias, + uint8_t* __restrict__ y_fp4, + uint8_t* __restrict__ y_sf, + int B, int C, int T, int H, int W, + int W_blocks_per_row, float eps) +{ + extern __shared__ __align__(16) char sm_buf[]; + // Strides must be multiples of 4 for uint32 read/write alignment. + const int sm_fp4_stride = ((C / 2) + 3) & ~3; + const int sm_sf_stride = ((C / 16) + 3) & ~3; + uint8_t* sm_fp4 = reinterpret_cast(sm_buf); + uint8_t* sm_sf = sm_fp4 + (size_t)kWBlock * sm_fp4_stride; + float* sm_red = reinterpret_cast( + sm_sf + (size_t)kWBlock * sm_sf_stride); + + const int wb = blockIdx.x % W_blocks_per_row; + const int rest = blockIdx.x / W_blocks_per_row; + const int hwt = T * H; + const int b = rest / hwt; + const int rh = rest - b * hwt; + const int t = rh / H; + const int h = rh - t * H; + if (b >= B) return; + + const int w_start = wb * kWBlock; + const int tx = threadIdx.x & 31; + const int ty = threadIdx.x >> 5; + const int my_w = w_start + tx; + const bool active = (my_w < W); + + const int c_per_y = (C + kThreadsY - 1) / kThreadsY; + const int my_c_start = ty * c_per_y; + const int my_c_end = min(my_c_start + c_per_y, C); + const int my_n_c = my_c_end - my_c_start; + const int my_n_pair = (my_n_c + 1) >> 1; + + const long long stride_C = (long long)T * H * W; + const long long row_off = (long long)t * H * W + (long long)h * W; + const long long b_off = (long long)b * (long long)C * stride_C; + // Channels-last: physical NDHWC, channel is innermost (coalesced) + const long long cl_row = ((long long)t * H + h) * W * C + (long long)my_w * C; + const long long cl_b_off = (long long)b * stride_C * C; + + // ── Pass 1: read x → register cache (half2) + sum_sq for RMS ── + __half2 xcache[kMaxHalf2]; + float sum_sq = 0.f; + if (active) { + #pragma unroll 1 + for (int p = 0; p < my_n_pair; ++p) { + int c0 = my_c_start + (p << 1); + int c1 = c0 + 1; + __half v0, v1; + if (kChannelsLast) { + // NDHWC physical: channel is innermost — coalesced reads + v0 = x[cl_b_off + cl_row + c0]; + v1 = (c1 < my_c_end) ? x[cl_b_off + cl_row + c1] : __float2half(0.f); + } else { + v0 = x[b_off + (long long)c0 * stride_C + row_off + my_w]; + v1 = (c1 < my_c_end) + ? x[b_off + (long long)c1 * stride_C + row_off + my_w] + : __float2half(0.f); + } + xcache[p] = __halves2half2(v0, v1); + if (kApplyNorm) { + float f0 = __half2float(v0); + float f1 = __half2float(v1); + sum_sq = fmaf(f0, f0, sum_sq); + if (c1 < my_c_end) sum_sq = fmaf(f1, f1, sum_sq); + } + } + } + + // ── RMS reduction (only when norm is applied) ── + float inv_rms = 0.f; + if (kApplyNorm) { + sm_red[ty * kThreadsX + tx] = active ? sum_sq : 0.f; + __syncthreads(); + float total = 0.f; + #pragma unroll + for (int yi = 0; yi < kThreadsY; ++yi) + total += sm_red[yi * kThreadsX + tx]; + inv_rms = active ? rsqrtf(total / static_cast(C) + eps) : 0.f; + } + + // ── Pass 2: norm(+bias)·SiLU → per-16-block FP4 quant → smem ── + if (active) { + const int blocks_per_thread = my_n_c / 16; + #pragma unroll 1 + for (int blk = 0; blk < blocks_per_thread; ++blk) { + // Compute norm+silu for this 16-element block, find max_abs + float vals[16]; + float mx = 0.f; + #pragma unroll + for (int p = 0; p < 8; ++p) { + int c0 = my_c_start + blk * 16 + p * 2; + int c1 = c0 + 1; + __half2 vp = xcache[blk * 8 + p]; + float f0, f1; + if (kApplyNorm) { + float xv0 = __half2float(__low2half(vp)); + float gv0 = __half2float(gamma[c0]); + float n0 = xv0 * inv_rms * gv0; + if (kApplyBias) n0 += __half2float(bias[c0]); + f0 = silu_f32(n0); + float xv1 = __half2float(__high2half(vp)); + float gv1 = __half2float(gamma[c1]); + float n1 = xv1 * inv_rms * gv1; + if (kApplyBias) n1 += __half2float(bias[c1]); + f1 = silu_f32(n1); + } else { + f0 = __half2float(__low2half(vp)); + f1 = __half2float(__high2half(vp)); + } + vals[p * 2] = f0; + vals[p * 2 + 1] = f1; + mx = fmaxf(mx, fmaxf(fabsf(f0), fabsf(f1))); + } + // Block scale + quantize + float sf_f = mx / 6.0f; + uint8_t sf_byte = fp32_to_ue4m3_ceil(sf_f); + float sf_dec = ue4m3_to_fp32(sf_byte); + float inv_sf = (sf_dec > 0.f) ? (1.0f / sf_dec) : 0.f; + int sf_idx = (my_c_start / 16) + blk; + sm_sf[tx * sm_sf_stride + sf_idx] = sf_byte; + #pragma unroll + for (int p = 0; p < 8; ++p) { + uint8_t lo = fp32_to_e2m1(vals[p * 2] * inv_sf); + uint8_t hi = fp32_to_e2m1(vals[p * 2 + 1] * inv_sf); + int byte_idx = (my_c_start / 2) + (blk * 8) + p; + sm_fp4[tx * sm_fp4_stride + byte_idx] = (hi << 4) | (lo & 0xF); + } + } + } + __syncthreads(); + + // ── Pass 3: coalesced uint32 global writes ── + const long long y_base_fp4 = ((long long)b * T * H * W + + (long long)t * H * W + + (long long)h * W + + w_start) * (long long)(C / 2); + const long long y_base_sf = ((long long)b * T * H * W + + (long long)t * H * W + + (long long)h * W + + w_start) * (long long)(C / 16); + + const int fp4_words_per_row = C / 8; + const int fp4_total_words = kWBlock * fp4_words_per_row; + const int tid = threadIdx.x; + #pragma unroll 1 + for (int idx = tid; idx < fp4_total_words; idx += kThreads) { + int w_off = idx / fp4_words_per_row; + int wd = idx - w_off * fp4_words_per_row; + if (w_start + w_off < W) { + uint32_t pack = *reinterpret_cast( + &sm_fp4[w_off * sm_fp4_stride + (wd << 2)]); + *reinterpret_cast( + &y_fp4[y_base_fp4 + (long long)w_off * (long long)(C / 2) + + (long long)(wd << 2)]) = pack; + } + } + { + const int sf_bytes_per_row = C / 16; + const int sf_words_per_row = sf_bytes_per_row / 4; + const int sf_remainder = sf_bytes_per_row & 3; + // uint32-aligned portion + if (sf_words_per_row > 0) { + const int sf_total_words = kWBlock * sf_words_per_row; + #pragma unroll 1 + for (int idx = tid; idx < sf_total_words; idx += kThreads) { + int w_off = idx / sf_words_per_row; + int wd = idx - w_off * sf_words_per_row; + if (w_start + w_off < W) { + uint32_t pack = *reinterpret_cast( + &sm_sf[w_off * sm_sf_stride + (wd << 2)]); + *reinterpret_cast( + &y_sf[y_base_sf + (long long)w_off * (long long)sf_bytes_per_row + + (long long)(wd << 2)]) = pack; + } + } + } + // Scalar remainder (handles C/16 not divisible by 4, e.g. C=96 → 6 bytes) + if (sf_remainder > 0) { + const int base = sf_words_per_row * 4; + const int rem_total = kWBlock * sf_remainder; + #pragma unroll 1 + for (int idx = tid; idx < rem_total; idx += kThreads) { + int w_off = idx / sf_remainder; + int bd = idx - w_off * sf_remainder; + if (w_start + w_off < W) { + y_sf[y_base_sf + (long long)w_off * (long long)sf_bytes_per_row + + (long long)base + bd] = + sm_sf[w_off * sm_sf_stride + base + bd]; + } + } + } + } +} + +inline int validate_and_smem(int B, int C, int T, int H, int W, size_t* smem) { + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + if ((C % 96) != 0) return -2; + if (C > 768) return -3; + if ((C / 16) < 4) return -4; + *smem = (size_t)kWBlock * (((C / 2) + 3) & ~3) + + (size_t)kWBlock * (((C / 16) + 3) & ~3) + + (size_t)kThreadsX * kThreadsY * 4; + return 0; +} + +} // namespace + +extern "C" int fp16_rms_silu_quant_nvfp4_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp4, + void* y_sf, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + size_t smem; + int rc = validate_and_smem(B, C, T, H, W, &smem); + if (rc != 0) return rc; + + const int W_blocks = (W + kWBlock - 1) / kWBlock; + const long long n_ctas = (long long)B * T * H * (long long)W_blocks; + if (n_ctas > (long long)INT32_MAX) return -5; + + dim3 grid(static_cast(n_ctas)); + dim3 block(kThreads); + + if (bias_fp16) { + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast(y_fp4), + reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, eps); + } else { + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + nullptr, + reinterpret_cast(y_fp4), + reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, eps); + } + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "[fp16_rms_silu_quant_nvfp4] launch err: %s\n", + cudaGetErrorString(e)); + return -10; + } + return 0; +} + +extern "C" int fp16_quant_nvfp4_ndhwc( + const void* x_fp16, + void* y_fp4, + void* y_sf, + int B, int C, int T, int H, int W, + cudaStream_t stream) +{ + size_t smem; + int rc = validate_and_smem(B, C, T, H, W, &smem); + if (rc != 0) return rc; + + const int W_blocks = (W + kWBlock - 1) / kWBlock; + const long long n_ctas = (long long)B * T * H * (long long)W_blocks; + if (n_ctas > (long long)INT32_MAX) return -5; + + dim3 grid(static_cast(n_ctas)); + dim3 block(kThreads); + + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + nullptr, nullptr, + reinterpret_cast(y_fp4), + reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, 0.f); + + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "[fp16_quant_nvfp4] launch err: %s\n", + cudaGetErrorString(e)); + return -10; + } + return 0; +} + +// ── Channels-last input variants (eliminates contiguous() copy) ── + +extern "C" int fp16_rms_silu_quant_nvfp4_cl_ndhwc( + const void* x_fp16, // channels-last 3D [B,C,T,H,W] physical NDHWC + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp4, + void* y_sf, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + size_t smem; + int rc = validate_and_smem(B, C, T, H, W, &smem); + if (rc != 0) return rc; + const int W_blocks = (W + kWBlock - 1) / kWBlock; + const long long n_ctas = (long long)B * T * H * (long long)W_blocks; + if (n_ctas > (long long)INT32_MAX) return -5; + dim3 grid(static_cast(n_ctas)); + dim3 block(kThreads); + if (bias_fp16) { + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast(y_fp4), reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, eps); + } else { + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), nullptr, + reinterpret_cast(y_fp4), reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, eps); + } + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { std::fprintf(stderr, "[rms_silu_quant_cl] err: %s\n", cudaGetErrorString(e)); return -10; } + return 0; +} + +extern "C" int fp16_quant_nvfp4_cl_ndhwc( + const void* x_fp16, // channels-last 3D [B,C,T,H,W] physical NDHWC + void* y_fp4, + void* y_sf, + int B, int C, int T, int H, int W, + cudaStream_t stream) +{ + size_t smem; + int rc = validate_and_smem(B, C, T, H, W, &smem); + if (rc != 0) return rc; + + const int W_blocks = (W + kWBlock - 1) / kWBlock; + const long long n_ctas = (long long)B * T * H * (long long)W_blocks; + if (n_ctas > (long long)INT32_MAX) return -5; + + dim3 grid(static_cast(n_ctas)); + dim3 block(kThreads); + + quant_nvfp4_kernel<<>>( + reinterpret_cast(x_fp16), + nullptr, nullptr, + reinterpret_cast(y_fp4), + reinterpret_cast(y_sf), + B, C, T, H, W, W_blocks, 0.f); + + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "[fp16_quant_nvfp4_cl] launch err: %s\n", + cudaGetErrorString(e)); + return -10; + } + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cuh b/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cuh new file mode 100644 index 00000000..c0db0708 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cuh @@ -0,0 +1,76 @@ +// FlashRT — MiniMax-Remover WanVAE NVFP4 fused quantization kernels. +// +// Two kernels: +// 1. fp16_rms_silu_quant_nvfp4_ndhwc: +// Fused RMS_norm + SiLU + NVFP4 quantization. +// fp16 NCDHW [B,C,T,H,W] → FP4 packed [B,T,H,W,C/2] + UE4M3 SF [B,T,H,W,C/16] +// Eliminates 3 separate passes (norm, silu, quant) into one kernel. +// +// 2. fp16_quant_nvfp4_ndhwc: +// Plain NVFP4 quantization (no norm/silu). +// fp16 NCDHW [B,C,T,H,W] → FP4 packed + UE4M3 SF (NDHWC layout). +// Used for causal-conv cache quantization. +// +// Output format matches motus_fp4_conv3d_v19sfb kernel input requirements: +// - FP4 data: [B,T,H,W, C/2] uint8 (2 e2m1 values packed per byte) +// - SF data: [B,T,H,W, C/16] uint8 (UE4M3 block scale, 1 per 16 elements) +// +// Thread layout: kThreadsX=32 (one per W position), kThreadsY=6 +// (chosen so C/6 is always a multiple of 16 for WanVAE channels 96/192/384: +// 96/6=16, 192/6=32, 384/6=64 — all clean SF-block multiples). +// +// Precision: RMS statistics accumulated in fp32 (bit-exact with WanRMS_norm). +// SiLU computed in fp32 (NOT rounded to bf16 like the motus variant — preserves +// fp16's 10-bit mantissa through the activation). FP4 quantization uses +// per-16-element UE4M3 block scales (same as NVFP4 hardware format). +#pragma once + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused: fp16 NCDHW → RMS_norm(+bias) → SiLU → NVFP4 quant (NDHWC out). +// Returns 0 on success, negative on error. +extern "C" int fp16_rms_silu_quant_nvfp4_ndhwc( + const void* x_fp16, // [B,C,T,H,W] __half + const void* gamma_fp16, // [C] __half + const void* bias_fp16, // [C] __half, or nullptr + void* y_fp4, // [B,T,H,W, C/2] uint8 packed + void* y_sf, // [B,T,H,W, C/16] uint8 UE4M3 + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream); + +// Plain: fp16 NCDHW → NVFP4 quant (NDHWC out). No norm/silu. +extern "C" int fp16_quant_nvfp4_ndhwc( + const void* x_fp16, // [B,C,T,H,W] __half NCDHW contiguous + void* y_fp4, // [B,T,H,W, C/2] uint8 packed + void* y_sf, // [B,T,H,W, C/16] uint8 UE4M3 + int B, int C, int T, int H, int W, + cudaStream_t stream); + +// Channels-last variant: fp16 channels-last 3D → NVFP4 quant (NDHWC out). +// Eliminates the contiguous() copy when input is already channels-last. +extern "C" int fp16_quant_nvfp4_cl_ndhwc( + const void* x_fp16, // [B,C,T,H,W] __half channels-last 3D + void* y_fp4, // [B,T,H,W, C/2] uint8 packed + void* y_sf, // [B,T,H,W, C/16] uint8 UE4M3 + int B, int C, int T, int H, int W, + cudaStream_t stream); + +// Fused RMS+SiLU+NVFP4 quant, channels-last input. +extern "C" int fp16_rms_silu_quant_nvfp4_cl_ndhwc( + const void* x_fp16, // [B,C,T,H,W] __half channels-last 3D + const void* gamma_fp16, // [C] __half + const void* bias_fp16, // [C] __half, or nullptr + void* y_fp4, // [B,T,H,W, C/2] uint8 packed + void* y_sf, // [B,T,H,W, C/16] uint8 UE4M3 + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cu b/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cu new file mode 100644 index 00000000..259b3fd4 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cu @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// Fused FP16 NCDHW RMSNorm for MiniMax-Remover VAE (Wan VAE). +// +// Replaces the diffusers WanRMS_norm.forward which, for fp16 inputs, does: +// x.float() -> F.normalize(x, dim=1) -> .to(fp16) -> * sqrt(C) * gamma + bias +// (four full-tensor passes, ~0.45 ms each on RTX 5060 Ti for [1,384,1,240,432]). +// +// This kernel is a single-pass-per-spatial-location fp16-native version: +// fp16 in, fp32 statistics, fp16 out -- NO dtype cast at all. The VAE stays +// in fp16 (cuDNN already dispatches fp16 tensorop conv kernels), so every +// RMS_norm call goes from ~0.45 ms to ~0.008 ms (measured), a ~56x speed-up. +// +// Precision: fp32 sum-of-squares accumulation, same as the bf16 sibling. +// fp16 has 10-bit mantissa vs bf16's 7-bit, so end-to-end VAE PSNR stays +// at ~40 dB vs the fp16 reference (vs ~15 dB for the bf16-cast path). + +#include "fp16_rms_norm_ncdhw.cuh" + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { +namespace { + +constexpr int kThreadsX = 32; +constexpr int kThreadsY = 8; +constexpr int kThreads = kThreadsX * kThreadsY; +constexpr int kMaxHalf2 = 64; // C <= 1024 with 8 y-lanes. + +__global__ void fp16_rms_norm_kernel( + const __half* __restrict__ x, + const __half* __restrict__ gamma, + const __half* __restrict__ bias, + __half* __restrict__ y, + int B, int C, int T, int H, int W, + int W_blocks_per_row, + float eps) +{ + __shared__ float sm_red[kThreads]; + + const int wb = blockIdx.x % W_blocks_per_row; + const int rest = blockIdx.x / W_blocks_per_row; + const int hwt = T * H; + const int b = rest / hwt; + const int rh = rest - b * hwt; + const int t = rh / H; + const int h = rh - t * H; + if (b >= B) return; + + const int tx = threadIdx.x & 31; + const int ty = threadIdx.x >> 5; + const int w = wb * kThreadsX + tx; + const bool active = (w < W); + + const int c_per_y = (C + kThreadsY - 1) / kThreadsY; + const int c_start = ty * c_per_y; + const int c_end = min(c_start + c_per_y, C); + const int n_c = c_end - c_start; + const int n_pair = (n_c + 1) >> 1; + + const long long stride_C = (long long)T * H * W; + const long long row_off = (long long)t * H * W + (long long)h * W + w; + const long long b_off = (long long)b * C * stride_C; + + __half2 xcache[kMaxHalf2]; + float sum_sq = 0.0f; + + if (active) { + #pragma unroll 1 + for (int p = 0; p < n_pair; ++p) { + int c0 = c_start + (p << 1); + int c1 = c0 + 1; + __half v0 = x[b_off + (long long)c0 * stride_C + row_off]; + __half v1 = (c1 < c_end) + ? x[b_off + (long long)c1 * stride_C + row_off] + : __float2half(0.0f); + xcache[p] = __half2{v0, v1}; + float f0 = __half2float(v0); + float f1 = __half2float(v1); + sum_sq = fmaf(f0, f0, sum_sq); + if (c1 < c_end) sum_sq = fmaf(f1, f1, sum_sq); + } + } + + sm_red[ty * kThreadsX + tx] = active ? sum_sq : 0.0f; + __syncthreads(); + + float total_sum_sq = 0.0f; + #pragma unroll + for (int yi = 0; yi < kThreadsY; ++yi) { + total_sum_sq += sm_red[yi * kThreadsX + tx]; + } + + const float inv_rms = active + ? rsqrtf(total_sum_sq * (1.0f / static_cast(C)) + eps) + : 0.0f; + + if (active) { + #pragma unroll 1 + for (int p = 0; p < n_pair; ++p) { + int c0 = c_start + (p << 1); + int c1 = c0 + 1; + __half2 vp = xcache[p]; + + float v0 = __half2float(vp.x) * inv_rms + * __half2float(gamma[c0]); + if (bias != nullptr) { + v0 += __half2float(bias[c0]); + } + y[b_off + (long long)c0 * stride_C + row_off] = + __float2half(v0); + + if (c1 < c_end) { + float v1 = __half2float(vp.y) * inv_rms + * __half2float(gamma[c1]); + if (bias != nullptr) { + v1 += __half2float(bias[c1]); + } + y[b_off + (long long)c1 * stride_C + row_off] = + __float2half(v1); + } + } + } +} + +} // namespace + +int fp16_rms_norm_ncdhw( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + if ((C & 1) != 0) return -2; + if (C > 1024) return -3; + + const int W_blocks_per_row = (W + kThreadsX - 1) / kThreadsX; + const long long n_ctas = + (long long)B * T * H * (long long)W_blocks_per_row; + if (n_ctas <= 0 || n_ctas > (long long)INT32_MAX) return -4; + + fp16_rms_norm_kernel<<(n_ctas), kThreads, 0, stream>>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast<__half*>(y_fp16), + B, C, T, H, W, W_blocks_per_row, eps); + return static_cast(cudaGetLastError()); +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cuh b/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cuh new file mode 100644 index 00000000..4c5eba75 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_norm_ncdhw.cuh @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused FP16 NCDHW RMSNorm for Wan VAE (MiniMax-Remover). +// fp16 in/out, fp32 statistics — no dtype cast. +// +// Computes: y = (x / rms(x)) * gamma + bias +// where rms(x) = sqrt(sum_c(x_c^2) / C + eps) +// which is algebraically identical to WanRMS_norm's +// F.normalize(x, dim=1) * sqrt(C) * gamma + bias. +// +// x_fp16: [B, C, T, H, W] fp16, NCDHW layout (C stride = T*H*W). +// gamma_fp16: [C] fp16 affine weight. +// bias_fp16: [C] fp16 or nullptr (zero bias). +// y_fp16: [B, C, T, H, W] fp16 output. +int fp16_rms_norm_ncdhw( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cu b/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cu new file mode 100644 index 00000000..1147e488 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cu @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Channels-last (NDHWC) RMSNorm + RMS_SiLU kernels for Wan VAE. + +#include "fp16_rms_norm_ndhwc.cuh" + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { +namespace { + +// One warp per spatial position. 4 warps per block (128 threads). +constexpr int kWarpsPerBlock = 4; +constexpr int kThreads = kWarpsPerBlock * 32; + +template +__global__ void fp16_rms_norm_cl_kernel( + const __half* __restrict__ x, + const __half* __restrict__ gamma, + const __half* __restrict__ bias, + __half* __restrict__ y, + int B, int C, int T, int H, int W, + int total_spatial, // B * T * H * W + float eps) +{ + const int warp_id = threadIdx.x >> 5; + const int lane = threadIdx.x & 31; + + const int spatial_idx = blockIdx.x * kWarpsPerBlock + warp_id; + if (spatial_idx >= total_spatial) return; + + // In NDHWC, C values for each spatial position are contiguous. + const long long base = (long long)spatial_idx * C; + const __half* x_row = x + base; + __half* y_row = y + base; + + // Phase 1: read C values, compute sum-of-squares. + float sum_sq = 0.0f; + for (int c = lane; c < C; c += 32) { + float v = __half2float(x_row[c]); + sum_sq = fmaf(v, v, sum_sq); + } + + // Warp-level reduction (no shared memory needed). + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + sum_sq += __shfl_xor_sync(0xFFFFFFFF, sum_sq, off); + + const float inv_rms = rsqrtf(sum_sq * (1.0f / static_cast(C)) + eps); + + // Phase 2: normalise, apply gamma+bias, optional silu, write. + for (int c = lane; c < C; c += 32) { + float v = __half2float(x_row[c]) * inv_rms + * __half2float(gamma[c]); + if (bias != nullptr) + v += __half2float(bias[c]); + if constexpr (kApplySilu) { + // silu(v) = v / (1 + exp(-v)) + v = v * (1.0f / (1.0f + __expf(-v))); + } + y_row[c] = __float2half(v); + } +} + +} // namespace + +int fp16_rms_norm_ndhwc( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + fp16_rms_norm_cl_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + reinterpret_cast<__half*>(y_fp16), + B, C, T, H, W, (int)total_spatial, eps); + return static_cast(cudaGetLastError()); +} + +int fp16_rms_silu_ndhwc( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + fp16_rms_norm_cl_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + reinterpret_cast<__half*>(y_fp16), + B, C, T, H, W, (int)total_spatial, eps); + return static_cast(cudaGetLastError()); +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cuh b/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cuh new file mode 100644 index 00000000..6a734d6e --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_norm_ndhwc.cuh @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Fused FP16 channels-last (NDHWC) RMSNorm for MiniMax-Remover VAE. +// +// When the VAE pipeline runs in channels-last 3D (NDHWC) memory format, +// cuDNN's conv3d skips the nchwToNhwc/nhwcToNchw conversion kernels +// (~287 ms / decode). This kernel keeps the norm output in NDHWC so +// the format is preserved end-to-end. +// +// In NDHWC, the C values for each spatial position (b, t, h, w) are +// CONTIGUOUS in memory — making the reduction over C much more cache- +// friendly than the NCDHW variant (where C values are strided by +// T*H*W). +// +// One warp (32 threads) per spatial position. Each thread handles +// C/32 values, then a warp-level __shfl reduction computes the RMS. +// No shared memory required. + +#pragma once + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// FP16 channels-last RMSNorm. +// x_fp16: [B, C, T, H, W] stored as NDHWC (C contiguous, stride 1). +// gamma_fp16: [C] or [C, 1, 1] (logical shape varies; data is just [C]). +// bias_fp16: [C] or nullptr. +// y_fp16: [B, C, T, H, W] NDHWC output. +// Computes: y = F.normalize(x, dim=1) * sqrt(C) * gamma + bias +// = (x / rms(x)) * gamma + bias (per-spatial-position) +int fp16_rms_norm_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +// FP16 channels-last RMSNorm + SiLU (fused). +// Computes: y = silu( (x / rms(x)) * gamma + bias ) +int fp16_rms_silu_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cu b/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cu new file mode 100644 index 00000000..95dacd7e --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cu @@ -0,0 +1,268 @@ +// ================================================================ +// flash_rt — MiniMax-Remover fused RMSNorm+SiLU with amax / FP8 +// quantize (channels-last NDHWC). +// +// Built on the same warp-per-spatial-position pattern as +// fp16_rms_norm_ndhwc, with three entry points: +// +// 1. fp16_rms_silu_amax_ndhwc — norm+silu+amax → fp16 + amax +// 2. fp16_rms_silu_quant_fp8_ndhwc — norm+silu+quant → fp8 (pre-comp amax) +// 3. fp16_rms_silu_amax_quant_fp8_ndhwc — 2-pass: → fp8 + scale +// +// Template kernel controls: +// kWriteFp16 : write the fp16 normed+silu output +// kComputeAmax : accumulate |output| via atomicMax +// kQuantizeFp8 : read device amax and write fp8 output +// ================================================================ + +#include "fp16_rms_silu_fp8_ndhwc.cuh" + +#include +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int kWarpsPerBlock = 4; +constexpr int kThreads = kWarpsPerBlock * 32; + +__device__ __forceinline__ float silu_f32(float x) { + return x * (1.0f / (1.0f + __expf(-x))); +} + +//atomicMax on reinterpreted int preserves ordering for non-negative floats. +__device__ __forceinline__ void atomic_max_f32(float* addr, float val) { + if (val > 0.0f) { + atomicMax(reinterpret_cast(addr), __float_as_int(val)); + } +} + +template +__global__ void fused_rms_silu_kernel( + const __half* __restrict__ x, + const __half* __restrict__ gamma, + const __half* __restrict__ bias, + __half* __restrict__ y_fp16, + __nv_fp8_e4m3* __restrict__ y_fp8, + float* __restrict__ amax_buf, + const float* __restrict__ amax_in, + float* __restrict__ scale_out, + int B, int C, int T, int H, int W, + int total_spatial, + float eps) +{ + const int warp_id = threadIdx.x >> 5; + const int lane = threadIdx.x & 31; + + const int spatial_idx = blockIdx.x * kWarpsPerBlock + warp_id; + if (spatial_idx >= total_spatial) return; + + const long long base = (long long)spatial_idx * C; + const __half* x_row = x + base; + + // ── Phase 1: read x, compute sum-of-squares ────────────────── + float sum_sq = 0.0f; + for (int c = lane; c < C; c += 32) { + float v = __half2float(x_row[c]); + sum_sq = fmaf(v, v, sum_sq); + } + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + sum_sq += __shfl_xor_sync(0xFFFFFFFF, sum_sq, off); + + const float inv_rms = + rsqrtf(sum_sq * (1.0f / static_cast(C)) + eps); + + // ── Optional: broadcast amax → inv_scale for FP8 quantize ──── + float inv_scale = 0.0f; + if constexpr (kQuantizeFp8) { + __shared__ float s_inv_scale; + if (threadIdx.x == 0) { + float amax = fmaxf(*amax_in, 0.0f); + float scale = amax * (1.0f / 448.0f); + if (scale < 1e-6f) scale = 1e-6f; + if (scale_out) *scale_out = scale; + s_inv_scale = 1.0f / scale; + } + __syncthreads(); + inv_scale = s_inv_scale; + } + + // ── Phase 2: normalise, apply gamma+bias, silu, write ──────── + float local_amax = 0.0f; + for (int c = lane; c < C; c += 32) { + float v = __half2float(x_row[c]) * inv_rms + * __half2float(gamma[c]); + if (bias != nullptr) + v += __half2float(bias[c]); + v = silu_f32(v); + + if constexpr (kWriteFp16) { + y_fp16[base + c] = __float2half_rn(v); + } + if constexpr (kComputeAmax) { + local_amax = fmaxf(local_amax, fabsf(v)); + } + if constexpr (kQuantizeFp8) { + float q = v * inv_scale; + q = fminf(fmaxf(q, -448.0f), 448.0f); + y_fp8[base + c] = __nv_fp8_e4m3(q); + } + } + + // ── Warp-level amax reduction + atomicMax ──────────────────── + if constexpr (kComputeAmax) { + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + local_amax = fmaxf(local_amax, __shfl_xor_sync(0xFFFFFFFF, local_amax, off)); + // lane 0 has the warp-wide amax + if (lane == 0 && local_amax > 0.0f) { + atomic_max_f32(amax_buf, local_amax); + } + } +} + +} // namespace + +// ── (1) Fused norm+silu+amax → fp16 + amax ────────────────────── +int fp16_rms_silu_amax_ndhwc( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp16, void* amax_buf, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + reinterpret_cast<__half*>(y_fp16), + nullptr, // y_fp8 + reinterpret_cast(amax_buf), + nullptr, nullptr, // amax_in, scale_out + B, C, T, H, W, (int)total_spatial, eps); + return static_cast(cudaGetLastError()); +} + +// ── (2) Fused norm+silu+quant → fp8 (pre-computed amax) ───────── +int fp16_rms_silu_quant_fp8_ndhwc( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp8, const void* amax_buf, void* scale_out, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + nullptr, // y_fp16 + reinterpret_cast<__nv_fp8_e4m3*>(y_fp8), + nullptr, // amax_buf (not computing) + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out), + B, C, T, H, W, (int)total_spatial, eps); + return static_cast(cudaGetLastError()); +} + +// ── (3) 2-pass: norm+silu+amax → fp8+scale (no fp16 write) ────── +int fp16_rms_silu_amax_quant_fp8_ndhwc( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp8, void* scale_out, void* amax_buf, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + cudaError_t e; + e = cudaMemsetAsync(amax_buf, 0, sizeof(float), stream); + if (e != cudaSuccess) return -2; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + + // Pass 1: norm+silu+amax (no write). + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + nullptr, + nullptr, + reinterpret_cast(amax_buf), + nullptr, nullptr, + B, C, T, H, W, (int)total_spatial, eps); + + // Pass 2: norm+silu+quant (reads amax from pass 1). + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + nullptr, + reinterpret_cast<__nv_fp8_e4m3*>(y_fp8), + nullptr, + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out), + B, C, T, H, W, (int)total_spatial, eps); + + e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -3; +} + +int fp16_rms_silu_amax_quant_fp8_ndhwc_nozero( + const void* x_fp16, const void* gamma_fp16, const void* bias_fp16, + void* y_fp8, void* scale_out, void* amax_buf, + int B, int C, int T, int H, int W, + float eps, cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + const long long total_spatial = (long long)B * T * H * W; + if (total_spatial <= 0 || total_spatial > (long long)INT32_MAX) return -4; + + const unsigned n_blocks = (unsigned)((total_spatial + kWarpsPerBlock - 1) + / kWarpsPerBlock); + + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + nullptr, + nullptr, + reinterpret_cast(amax_buf), + nullptr, nullptr, + B, C, T, H, W, (int)total_spatial, eps); + + fused_rms_silu_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + nullptr, + reinterpret_cast<__nv_fp8_e4m3*>(y_fp8), + nullptr, + reinterpret_cast(amax_buf), + reinterpret_cast(scale_out), + B, C, T, H, W, (int)total_spatial, eps); + + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -3; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cuh b/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cuh new file mode 100644 index 00000000..8111832e --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cuh @@ -0,0 +1,92 @@ +// ================================================================ +// flash_rt — MiniMax-Remover fused RMSNorm+SiLU with FP8 quantize +// (channels-last NDHWC). +// +// Three entry points, all built on the same warp-per-spatial template: +// +// 1. fp16_rms_silu_amax_ndhwc +// Fused norm+silu+amax. Reads fp16 x, writes fp16 y AND +// accumulates |y| into a device-side amax buffer via atomicMax. +// Saves one full read of y compared to (norm+silu) → separate amax. +// +// 2. fp16_rms_silu_quant_fp8_ndhwc +// Fused norm+silu+quantize-to-FP8. Reads fp16 x, reads a +// pre-computed amax from device memory, quantizes and writes +// fp8 y. Does NOT write fp16 output — eliminates the fp16 +// intermediate entirely when the consumer only needs FP8. +// +// 3. fp16_rms_silu_amax_quant_fp8_ndhwc +// 2-pass launcher combining (1) and (2): pass 1 computes +// norm+silu+amax (no fp16 write); pass 2 re-reads x, computes +// norm+silu+quant. Produces ONLY fp8 output + scale. +// +// Use case: in WanResidualBlock the pattern is +// x → norm → silu → conv1(quant→FP8→MMA) +// Fusing norm+silu+amax+quant eliminates the fp16 intermediate +// between norm and conv, saving one full read+write per layer. +// ================================================================ +#pragma once +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// (1) Fused norm+silu+amax → fp16 output + device amax. +// amax_buf must be zeroed by the caller before the first call. +// Multiple calls accumulate (atomicMax). +int fp16_rms_silu_amax_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + void* amax_buf, // device float *, caller-zeroed + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +// (2) Fused norm+silu+quant → fp8 output (reads pre-computed amax). +// No fp16 output is written. +int fp16_rms_silu_quant_fp8_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp8, // __nv_fp8_e4m3 * + const void* amax_buf, // device float *, pre-computed + void* scale_out, // device float *, may be nullptr + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +// (3) 2-pass: norm+silu+amax+quant → fp8 output + scale. +// amax_buf is a caller-provided scratch (1 float). +// No fp16 output. +int fp16_rms_silu_amax_quant_fp8_ndhwc( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp8, + void* scale_out, + void* amax_buf, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +// (3b) Same as (3) but does NOT zero amax_buf before pass 1. +// For running-max mode: caller seeds amax_buf with the historical +// running max; pass 1 atomicMax-accumulates the current output's +// amax; pass 2 quantizes with max(historical, current). +int fp16_rms_silu_amax_quant_fp8_ndhwc_nozero( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp8, + void* scale_out, + void* amax_buf, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cu b/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cu new file mode 100644 index 00000000..c9764b02 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cu @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +// Fused FP16 NCDHW RMSNorm + SiLU for MiniMax-Remover VAE (Wan VAE). +// +// In every WanResidualBlock the pattern is: +// x = self.norm1(x) # WanRMS_norm (fp32 stats, 4 full passes) +// x = self.nonlinearity(x) # SiLU (another full pass) +// This kernel fuses both into a single pass: fp16 in, fp32 stats + +// activation, fp16 out -- NO dtype cast and NO intermediate tensor. +// +// Saves one full read+write of the activation tensor plus one kernel +// launch per site (~522 sites/decode). Measured ~1.3x faster than the +// unfused fp16_rms_norm_ncdhw + aten::silu pair, on top of the existing +// ~6x over the original WanRMS_norm. + +#include "fp16_rms_silu_ncdhw.cuh" + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { +namespace { + +constexpr int kThreadsX = 32; +constexpr int kThreadsY = 8; +constexpr int kThreads = kThreadsX * kThreadsY; +constexpr int kMaxHalf2 = 64; // C <= 1024 with 8 y-lanes. + +__device__ __forceinline__ float silu_f32(float x) { + return x * (1.0f / (1.0f + __expf(-x))); +} + +__global__ void fp16_rms_silu_kernel( + const __half* __restrict__ x, + const __half* __restrict__ gamma, + const __half* __restrict__ bias, + __half* __restrict__ y, + int B, int C, int T, int H, int W, + int W_blocks_per_row, + float eps) +{ + __shared__ float sm_red[kThreads]; + + const int wb = blockIdx.x % W_blocks_per_row; + const int rest = blockIdx.x / W_blocks_per_row; + const int hwt = T * H; + const int b = rest / hwt; + const int rh = rest - b * hwt; + const int t = rh / H; + const int h = rh - t * H; + if (b >= B) return; + + const int tx = threadIdx.x & 31; + const int ty = threadIdx.x >> 5; + const int w = wb * kThreadsX + tx; + const bool active = (w < W); + + const int c_per_y = (C + kThreadsY - 1) / kThreadsY; + const int c_start = ty * c_per_y; + const int c_end = min(c_start + c_per_y, C); + const int n_c = c_end - c_start; + const int n_pair = (n_c + 1) >> 1; + + const long long stride_C = (long long)T * H * W; + const long long row_off = (long long)t * H * W + (long long)h * W + w; + const long long b_off = (long long)b * C * stride_C; + + __half2 xcache[kMaxHalf2]; + float sum_sq = 0.0f; + + if (active) { + #pragma unroll 1 + for (int p = 0; p < n_pair; ++p) { + int c0 = c_start + (p << 1); + int c1 = c0 + 1; + __half v0 = x[b_off + (long long)c0 * stride_C + row_off]; + __half v1 = (c1 < c_end) + ? x[b_off + (long long)c1 * stride_C + row_off] + : __float2half(0.0f); + xcache[p] = __half2{v0, v1}; + float f0 = __half2float(v0); + float f1 = __half2float(v1); + sum_sq = fmaf(f0, f0, sum_sq); + if (c1 < c_end) sum_sq = fmaf(f1, f1, sum_sq); + } + } + + sm_red[ty * kThreadsX + tx] = active ? sum_sq : 0.0f; + __syncthreads(); + + float total_sum_sq = 0.0f; + #pragma unroll + for (int yi = 0; yi < kThreadsY; ++yi) { + total_sum_sq += sm_red[yi * kThreadsX + tx]; + } + + const float inv_rms = active + ? rsqrtf(total_sum_sq * (1.0f / static_cast(C)) + eps) + : 0.0f; + + if (active) { + #pragma unroll 1 + for (int p = 0; p < n_pair; ++p) { + int c0 = c_start + (p << 1); + int c1 = c0 + 1; + __half2 vp = xcache[p]; + + float n0 = __half2float(vp.x) * inv_rms + * __half2float(gamma[c0]); + if (bias != nullptr) { + n0 += __half2float(bias[c0]); + } + y[b_off + (long long)c0 * stride_C + row_off] = + __float2half(silu_f32(n0)); + + if (c1 < c_end) { + float n1 = __half2float(vp.y) * inv_rms + * __half2float(gamma[c1]); + if (bias != nullptr) { + n1 += __half2float(bias[c1]); + } + y[b_off + (long long)c1 * stride_C + row_off] = + __float2half(silu_f32(n1)); + } + } + } +} + +} // namespace + +int fp16_rms_silu_ncdhw( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream) +{ + if (B <= 0 || C <= 0 || T <= 0 || H <= 0 || W <= 0) return -1; + if ((C & 1) != 0) return -2; + if (C > 1024) return -3; + + const int W_blocks_per_row = (W + kThreadsX - 1) / kThreadsX; + const long long n_ctas = + (long long)B * T * H * (long long)W_blocks_per_row; + if (n_ctas <= 0 || n_ctas > (long long)INT32_MAX) return -4; + + fp16_rms_silu_kernel<<(n_ctas), kThreads, 0, stream>>>( + reinterpret_cast(x_fp16), + reinterpret_cast(gamma_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast<__half*>(y_fp16), + B, C, T, H, W, W_blocks_per_row, eps); + return static_cast(cudaGetLastError()); +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cuh b/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cuh new file mode 100644 index 00000000..066e488d --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rms_silu_ncdhw.cuh @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused FP16 NCDHW RMSNorm + SiLU for Wan VAE (MiniMax-Remover) residual +// blocks, where every WanRMS_norm is immediately followed by a SiLU +// activation. Fusing the two ops into one kernel pass eliminates one full +// tensor read + write (the intermediate norm output) and one kernel launch +// per site (~522 sites/decode). +// +// Computes: y = silu( (x / rms(x)) * gamma + bias ) +// where rms(x) = sqrt(sum_c(x_c^2) / C + eps) +// and silu(v) = v / (1 + exp(-v)) +// which equals WanRMS_norm.forward(x) followed by F.silu. +// +// fp16 in/out, fp32 statistics + activation -- NO dtype cast. +// +// x_fp16: [B, C, T, H, W] fp16, NCDHW layout (C stride = T*H*W). +// gamma_fp16: [C] fp16 affine weight. +// bias_fp16: [C] fp16 or nullptr (zero bias). +// y_fp16: [B, C, T, H, W] fp16 output. +int fp16_rms_silu_ncdhw( + const void* x_fp16, + const void* gamma_fp16, + const void* bias_fp16, + void* y_fp16, + int B, int C, int T, int H, int W, + float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cu b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cu new file mode 100644 index 00000000..f74cefe7 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cu @@ -0,0 +1,196 @@ +// ================================================================ +// flash_rt_minimax_remover — fused RMSNorm + interleaved RoPE. +// See fp16_rmsnorm_rope.cuh for semantics. +// +// Implementation: +// * One CUDA block per token (B*S tokens total). Each block +// reduces D in fp32 across THREADS threads, then applies affine +// weight + interleaved RoPE. +// * fp16x8 (uint4) vector loads/stores for x. +// * cos/sin tables indexed by (seq_idx, pair_idx) — same table +// across heads, so a per-token block re-uses the same base_cos_sin +// pointer for each head. +// ================================================================ +#include "fp16_rmsnorm_rope.cuh" + +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int VEC = 8; + +__device__ __forceinline__ float warp_reduce_sum(float v) { + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + v += __shfl_xor_sync(0xffffffff, v, off); + return v; +} + +template +__global__ void rmsnorm_rope_kernel( + __half* __restrict__ x, + const __half* __restrict__ weight, + const float* __restrict__ cos_tab, + const float* __restrict__ sin_tab, + int B, int S, int H, int Dd, + float eps) +{ + const int tok = blockIdx.x; // 0 .. B*S - 1 + const int s = tok % S; + const int tid = threadIdx.x; + const int lane = tid & 31; + const int warp = tid >> 5; + constexpr int NWARPS = THREADS / 32; + + const int D = H * Dd; + __half* xrow = x + tok * D; + + // ── Pass 1: sum of squares over full D (across all heads) ──────── + const int D_vec = D / VEC; + float local_sq = 0.f; + for (int v = tid; v < D_vec; v += THREADS) { + const uint4* p = reinterpret_cast(xrow + v * VEC); + uint4 raw = *p; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + local_sq += f0.x*f0.x + f0.y*f0.y + f1.x*f1.x + f1.y*f1.y + + f2.x*f2.x + f2.y*f2.y + f3.x*f3.x + f3.y*f3.y; + } + __shared__ float smem[NWARPS]; + float ws = warp_reduce_sum(local_sq); + if (lane == 0) smem[warp] = ws; + __syncthreads(); + float rstd; + if (warp == 0) { + float v = (lane < NWARPS) ? smem[lane] : 0.f; + v = warp_reduce_sum(v); + if (lane == 0) smem[0] = rsqrtf(v / (float)D + eps); + } + __syncthreads(); + rstd = smem[0]; + + // ── Pass 2: (x*rstd*w) then RoPE within each head ──────────────── + // + // Layout: xrow[h*Dd + i], for h in [0,H), i in [0,Dd). + // Weight[h*Dd + i] provides the affine multiplier. + // RoPE pairs (i, i+1) share cos[s, i/2], sin[s, i/2] across h. + // + // Process VEC=8 elements per thread, which spans (VEC/2)=4 RoPE + // pairs. Guarantee Dd is a multiple of VEC so within-head pair + // alignment holds. Since D = H*Dd is a multiple of VEC (H*Dd + // multiple of 8 required by caller — H%1 free, Dd%8 required), + // vector index v corresponds to head h = (v*VEC) / Dd and offset + // off_in_head = (v*VEC) % Dd. + const float* cos_row = cos_tab + s * (Dd / 2); + const float* sin_row = sin_tab + s * (Dd / 2); + for (int v = tid; v < D_vec; v += THREADS) { + const int base_d = v * VEC; + const int off = base_d % Dd; // 0 .. Dd-1 within head + // Load x, weight + const uint4* px = reinterpret_cast(xrow + base_d); + uint4 raw = *px; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + + const uint4* pw = reinterpret_cast(weight + base_d); + uint4 wraw = *pw; + __half2 w0 = *reinterpret_cast<__half2*>(&wraw.x); + __half2 w1 = *reinterpret_cast<__half2*>(&wraw.y); + __half2 w2 = *reinterpret_cast<__half2*>(&wraw.z); + __half2 w3 = *reinterpret_cast<__half2*>(&wraw.w); + + // Normalise + affine (fp32) + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + float2 wf0 = __half22float2(w0); + float2 wf1 = __half22float2(w1); + float2 wf2 = __half22float2(w2); + float2 wf3 = __half22float2(w3); + float xv[VEC]; + xv[0] = f0.x * rstd * wf0.x; + xv[1] = f0.y * rstd * wf0.y; + xv[2] = f1.x * rstd * wf1.x; + xv[3] = f1.y * rstd * wf1.y; + xv[4] = f2.x * rstd * wf2.x; + xv[5] = f2.y * rstd * wf2.y; + xv[6] = f3.x * rstd * wf3.x; + xv[7] = f3.y * rstd * wf3.y; + + // Load cos/sin for the 4 pairs (off, off+2, off+4, off+6). + // These are within the same head (guaranteed since Dd % VEC == 0). + const int pair0 = off / 2; + float cs[4], sn[4]; + // 4 fp32 pairs; use float4 loads. + const float4* pc = reinterpret_cast(cos_row + pair0); + const float4* ps = reinterpret_cast(sin_row + pair0); + float4 c4 = *pc, s4 = *ps; + cs[0] = c4.x; cs[1] = c4.y; cs[2] = c4.z; cs[3] = c4.w; + sn[0] = s4.x; sn[1] = s4.y; sn[2] = s4.z; sn[3] = s4.w; + + // Apply RoPE (interleaved): (x0, x1) -> (x0*c - x1*s, x0*s + x1*c) + #pragma unroll + for (int j = 0; j < 4; j++) { + float x0 = xv[2*j], x1 = xv[2*j + 1]; + float c = cs[j], sn_j = sn[j]; + xv[2*j ] = x0 * c - x1 * sn_j; + xv[2*j + 1] = x0 * sn_j + x1 * c; + } + + // Pack back to fp16 and store. + __half2 o0 = __floats2half2_rn(xv[0], xv[1]); + __half2 o1 = __floats2half2_rn(xv[2], xv[3]); + __half2 o2 = __floats2half2_rn(xv[4], xv[5]); + __half2 o3 = __floats2half2_rn(xv[6], xv[7]); + uint4 out; + out.x = *reinterpret_cast(&o0); + out.y = *reinterpret_cast(&o1); + out.z = *reinterpret_cast(&o2); + out.w = *reinterpret_cast(&o3); + *reinterpret_cast(xrow + base_d) = out; + } +} + +} // anonymous namespace + +int fp16_rmsnorm_rope_bshd( + void* x_fp16, + const void* weight_fp16, + const void* cos_fp32, + const void* sin_fp32, + int B, int S, int H, int Dd, + float eps, + cudaStream_t stream) +{ + if (!x_fp16 || !weight_fp16 || !cos_fp32 || !sin_fp32) return -1; + if (B <= 0 || S <= 0 || H <= 0 || Dd <= 0) return -2; + const int D = H * Dd; + if ((D % VEC) != 0 || (Dd % VEC) != 0) return -3; + constexpr int THREADS = 128; + const int tokens = B * S; + rmsnorm_rope_kernel<<>>( + reinterpret_cast<__half*>(x_fp16), + reinterpret_cast(weight_fp16), + reinterpret_cast(cos_fp32), + reinterpret_cast(sin_fp32), + B, S, H, Dd, eps); + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cuh b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cuh new file mode 100644 index 00000000..beba6a47 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope.cuh @@ -0,0 +1,54 @@ +// ================================================================ +// flash_rt_minimax_remover — fused RMSNorm + interleaved RoPE kernel. +// +// The MiniMax-Remover attention entry does three separate passes over +// each Q/K tensor between the QKV Linear and the attention kernel: +// +// 1. rms_norm_fp32stat(q, norm_q.weight, eps) # fp32-stat RMSNorm +// - reads q, computes per-token RMS across D, writes normalised +// 2. .view(B, S, H, Dd) # zero-cost reshape +// 3. rope_apply_bshd(q, cos, sin) # in-place interleaved RoPE +// +// This kernel fuses (1) and (3) into a single pass: +// * Per-token fp32 RMS reduction over D (D = H * Dd). +// * Per-element affine (weight[d] broadcast across [B*S] rows). +// * Interleaved RoPE on adjacent (2k, 2k+1) fp16 pairs, with +// per-head cos/sin tables shared across heads (cos/sin depend +// only on the sequence index and the pair index within Dd). +// +// Layout assumptions (matches _kernels.py): +// x : [B*S*H, Dd] fp16 contiguous (equivalent to [B,S,H,Dd]) +// weight : [D] fp16 (RMSNorm affine, D = H * Dd) +// cos, sin : [S, Dd/2] fp32 +// eps : LayerNorm epsilon +// +// The RMS is computed over the *full* D per token (i.e. jointly over +// all heads), matching qk_norm="rms_norm_across_heads". Grid: one +// block per token (B*S), each block reduces D in fp32 then applies +// RoPE across the H heads sequentially. +// ================================================================ +#ifndef FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_RMSNORM_ROPE_CUH +#define FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_RMSNORM_ROPE_CUH + +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused RMSNorm(fp32 stats + fp16 affine) + interleaved RoPE. +// x is treated as [B*S, H, Dd] fp16 contiguous. Returns 0 on success. +int fp16_rmsnorm_rope_bshd( + void* x_fp16, // in/out [B*S, D] D = H*Dd + const void* weight_fp16, // [D] + const void* cos_fp32, // [S, Dd/2] + const void* sin_fp32, // [S, Dd/2] + int B, int S, int H, int Dd, + float eps, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt + +#endif // FLASHRT_KERNELS_MINIMAX_REMOVER_FP16_RMSNORM_ROPE_CUH diff --git a/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cu b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cu new file mode 100644 index 00000000..56e90ee3 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cu @@ -0,0 +1,352 @@ +// ================================================================ +// Fused RMSNorm + interleaved RoPE + int8 per-warp/per-block quantization. +// +// Two-kernel pipeline: +// Kernel A (rmsnorm_rstd_kernel): 1 block/token, computes rstd across +// D=H*Dd and writes to rstd[B*S] global buffer. +// Kernel B (norm_rope_quant_kernel): 1 block per (group, head, batch), +// reads x fp16 + rstd, applies norm+rope, reduces max-abs across +// the group, quantizes to int8 in one pass over shared memory. +// +// Eliminates the intermediate fp16 tensor between rmsnorm+rope and +// the int8 quantization — saves 2×S×D bytes per Q/K tensor (read+write). +// ================================================================ + +#include "fp16_rmsnorm_rope_quant_int8.cuh" +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int VEC_RSTD = 8; + +__device__ __forceinline__ float warp_reduce_sum_rq(float v) { + #pragma unroll + for (int off = 16; off > 0; off >>= 1) + v += __shfl_xor_sync(0xffffffff, v, off); + return v; +} + +// ─── Kernel A: compute rstd per token ───────────────────────────── +// Grid: (B*S), Block: 128 threads +// Reads D fp16 elements, reduces sum-of-squares, writes 1 float. +template +__global__ void rmsnorm_rstd_kernel( + const __half* __restrict__ x, // [B*S, D] + const __half* __restrict__ bias, // [D] or nullptr — added before rmsnorm + float* __restrict__ rstd_out, // [B*S] + int D, float eps) +{ + const int tok = blockIdx.x; + const int tid = threadIdx.x; + const int lane = tid & 31; + const int warp = tid >> 5; + constexpr int NWARPS = THREADS / 32; + + const __half* xrow = x + (long long)tok * D; + + float local_sq = 0.f; + const int D_vec = D / VEC_RSTD; + for (int v = tid; v < D_vec; v += THREADS) { + const uint4* p = reinterpret_cast(xrow + v * VEC_RSTD); + uint4 raw = *p; + __half2 h0 = *reinterpret_cast<__half2*>(&raw.x); + __half2 h1 = *reinterpret_cast<__half2*>(&raw.y); + __half2 h2 = *reinterpret_cast<__half2*>(&raw.z); + __half2 h3 = *reinterpret_cast<__half2*>(&raw.w); + float2 f0 = __half22float2(h0); + float2 f1 = __half22float2(h1); + float2 f2 = __half22float2(h2); + float2 f3 = __half22float2(h3); + if (bias) { + const uint4* pb = reinterpret_cast(bias + v * VEC_RSTD); + uint4 rb = *pb; + float2 b0 = __half22float2(*reinterpret_cast<__half2*>(&rb.x)); + float2 b1 = __half22float2(*reinterpret_cast<__half2*>(&rb.y)); + float2 b2 = __half22float2(*reinterpret_cast<__half2*>(&rb.z)); + float2 b3 = __half22float2(*reinterpret_cast<__half2*>(&rb.w)); + f0.x += b0.x; f0.y += b0.y; f1.x += b1.x; f1.y += b1.y; + f2.x += b2.x; f2.y += b2.y; f3.x += b3.x; f3.y += b3.y; + } + local_sq += f0.x*f0.x + f0.y*f0.y + f1.x*f1.x + f1.y*f1.y + + f2.x*f2.x + f2.y*f2.y + f3.x*f3.x + f3.y*f3.y; + } + + __shared__ float smem[NWARPS]; + float ws = warp_reduce_sum_rq(local_sq); + if (lane == 0) smem[warp] = ws; + __syncthreads(); + + if (warp == 0) { + float v = (lane < NWARPS) ? smem[lane] : 0.f; + v = warp_reduce_sum_rq(v); + if (lane == 0) + rstd_out[tok] = rsqrtf(v / (float)D + eps); + } +} + +// ─── Kernel B: norm + rope + max-abs + quantize ────────────────── +// Grid: (num_groups, H, B) +// Block: TPB threads (256) +// Each block processes GROUP_SIZE tokens × Dd dims for one head. +// +// Shared memory: +// normed[GROUP_SIZE * Dd] fp32 — holds normalized+RoPE'd values +// reduce_buf[TPB] fp32 — for max-abs reduction +template +__global__ void norm_rope_quant_kernel( + const __half* __restrict__ x, // [B*S, D] + const __half* __restrict__ weight, // [D] + const __half* __restrict__ bias, // [D] or nullptr — added before rmsnorm + const float* __restrict__ cos_tab, // [S, Dd/2] + const float* __restrict__ sin_tab, // [S, Dd/2] + const float* __restrict__ rstd, // [B*S] + const __half* __restrict__ km, // [B, H, Dd] or nullptr + int8_t* __restrict__ out, // [B*S, D] + float* __restrict__ scale_out, // [B, H, num_groups] + int B, int S, int H, int Dd, + float sm_scale_factor) +{ + const int group_idx = blockIdx.x; + const int h = blockIdx.y; + const int b = blockIdx.z; + const int tid = threadIdx.x; + const int D = H * Dd; + + const int tok_start = group_idx * GROUP_SIZE; + const int tok_end = min(tok_start + GROUP_SIZE, S); + const int n_toks = tok_end - tok_start; + const int total_elems = n_toks * Dd; + + extern __shared__ char smem_raw[]; + float* normed = reinterpret_cast(smem_raw); + float* reduce_buf = normed + GROUP_SIZE * Dd; + + // ── Phase 1: Load, normalize, apply RoPE, store to shared ── + float local_max = 0.f; + + // Process elements in pairs (for RoPE correctness) + // Each element at even dim d needs its partner at d+1 and vice versa. + // Process in pairs: iterate over (tok, pair_idx) where pair_idx = d/2. + const int total_pairs = n_toks * (Dd / 2); + + for (int idx = tid; idx < total_pairs; idx += TPB) { + const int lt = idx / (Dd / 2); // local token + const int pair = idx % (Dd / 2); // pair index within head + const int d0 = pair * 2; + const int d1 = d0 + 1; + const int s = tok_start + lt; // sequence position + const int global_tok = b * S + s; + + const float my_rstd = rstd[global_tok]; + + // Load x values (+ bias, fused: avoids the separate add_bias kernel) + float x0 = __half2float(x[(long long)global_tok * D + h * Dd + d0]); + float x1 = __half2float(x[(long long)global_tok * D + h * Dd + d1]); + if (bias) { + x0 += __half2float(bias[h * Dd + d0]); + x1 += __half2float(bias[h * Dd + d1]); + } + + // Load weights + float w0 = __half2float(weight[h * Dd + d0]); + float w1 = __half2float(weight[h * Dd + d1]); + + // RMSNorm + affine + x0 = x0 * my_rstd * w0; + x1 = x1 * my_rstd * w1; + + // Interleaved RoPE: (x0, x1) → (x0*c - x1*s, x0*s + x1*c) + float c = cos_tab[s * (Dd / 2) + pair]; + float sn = sin_tab[s * (Dd / 2) + pair]; + float r0 = x0 * c - x1 * sn; + float r1 = x0 * sn + x1 * c; + + // Apply sm_scale (for Q quantization, folds softmax scale) + r0 *= sm_scale_factor; + r1 *= sm_scale_factor; + + // Subtract key mean if applicable (smooth_k) + if (km != nullptr) { + float km0 = __half2float(km[(long long)b * H * Dd + h * Dd + d0]); + float km1 = __half2float(km[(long long)b * H * Dd + h * Dd + d1]); + r0 -= km0 * sm_scale_factor; + r1 -= km1 * sm_scale_factor; + } + + // Store to shared memory + normed[lt * Dd + d0] = r0; + normed[lt * Dd + d1] = r1; + + // Track max absolute value + local_max = fmaxf(local_max, fmaxf(fabsf(r0), fabsf(r1))); + } + __syncthreads(); + + // ── Phase 2: Reduce max-abs across all threads ── + reduce_buf[tid] = local_max; + __syncthreads(); + for (int stride = TPB / 2; stride > 0; stride >>= 1) { + if (tid < stride) + reduce_buf[tid] = fmaxf(reduce_buf[tid], reduce_buf[tid + stride]); + __syncthreads(); + } + float group_max = reduce_buf[0]; + float scale = group_max / 127.0f + 1e-7f; + float inv_scale = 1.0f / scale; + + // Write scale + if (tid == 0) { + const int num_groups = (S + GROUP_SIZE - 1) / GROUP_SIZE; + scale_out[(long long)b * H * num_groups + h * num_groups + group_idx] = scale; + } + + // ── Phase 3: Quantize from shared memory and write int8 ── + for (int idx = tid; idx < total_elems; idx += TPB) { + const int lt = idx / Dd; + const int d = idx % Dd; + const int global_tok = b * S + tok_start + lt; + + float val = normed[lt * Dd + d]; + int ival = __float2int_rn(val * inv_scale); + ival = max(-128, min(127, ival)); + out[(long long)global_tok * D + h * Dd + d] = static_cast(ival); + } +} + +} // anonymous namespace + +int fp16_rmsnorm_rope_quant_int8_q( + const void* x_fp16, + const void* weight_fp16, + const void* bias_fp16, + const void* cos_fp32, + const void* sin_fp32, + void* out_int8, + void* scale_fp32, + int B, int S, int H, int Dd, + float eps, float sm_scale, + void* rstd_buf, + cudaStream_t stream) +{ + if (!x_fp16 || !weight_fp16 || !cos_fp32 || !sin_fp32 || + !out_int8 || !scale_fp32) + return -1; + if (B <= 0 || S <= 0 || H <= 0 || Dd <= 0) return -2; + const int D = H * Dd; + if ((D % VEC_RSTD) != 0) return -3; + + // rstd scratch: prefer the caller-owned buffer (hot path, zero alloc). + // Fall back to a stream-ordered transient allocation only when the + // caller passes nullptr (one-off / non-hot-path use). + float* rstd = static_cast(rstd_buf); + float* transient = nullptr; + if (rstd == nullptr) { + cudaError_t e = cudaMallocAsync(&transient, + B * S * sizeof(float), stream); + if (e != cudaSuccess) return -4; + rstd = transient; + } + + // Kernel A: compute rstd (over x+bias) + constexpr int THREADS_A = 128; + rmsnorm_rstd_kernel<<>>( + reinterpret_cast(x_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + rstd, D, eps); + + // Kernel B: norm + rope + quantize (Q: GROUP_SIZE=32) + constexpr int GROUP_SIZE = 32; + constexpr int TPB = 256; + const int num_groups = (S + GROUP_SIZE - 1) / GROUP_SIZE; + dim3 grid(num_groups, H, B); + size_t smem = (GROUP_SIZE * Dd + TPB) * sizeof(float); + + norm_rope_quant_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(weight_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + reinterpret_cast(cos_fp32), + reinterpret_cast(sin_fp32), + rstd, + nullptr, // no km for Q + reinterpret_cast(out_int8), + reinterpret_cast(scale_fp32), + B, S, H, Dd, sm_scale); + + if (transient != nullptr) + cudaFreeAsync(transient, stream); + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -5; +} + +int fp16_rmsnorm_rope_quant_int8_k( + const void* x_fp16, + const void* weight_fp16, + const void* bias_fp16, + const void* cos_fp32, + const void* sin_fp32, + const void* km_fp16, + void* out_int8, + void* scale_fp32, + int B, int S, int H, int Dd, + float eps, float sm_scale, + void* rstd_buf, + cudaStream_t stream) +{ + if (!x_fp16 || !weight_fp16 || !cos_fp32 || !sin_fp32 || + !out_int8 || !scale_fp32) + return -1; + if (B <= 0 || S <= 0 || H <= 0 || Dd <= 0) return -2; + const int D = H * Dd; + if ((D % VEC_RSTD) != 0) return -3; + + // rstd scratch: prefer the caller-owned buffer (hot path, zero alloc). + float* rstd = static_cast(rstd_buf); + float* transient = nullptr; + if (rstd == nullptr) { + cudaError_t e = cudaMallocAsync(&transient, + B * S * sizeof(float), stream); + if (e != cudaSuccess) return -4; + rstd = transient; + } + + // Kernel A: compute rstd (over x+bias) + constexpr int THREADS_A = 128; + rmsnorm_rstd_kernel<<>>( + reinterpret_cast(x_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + rstd, D, eps); + + // Kernel B: norm + rope + quantize (K: GROUP_SIZE=64, with smooth_k) + constexpr int GROUP_SIZE = 64; + constexpr int TPB = 256; + const int num_groups = (S + GROUP_SIZE - 1) / GROUP_SIZE; + dim3 grid(num_groups, H, B); + size_t smem = (GROUP_SIZE * Dd + TPB) * sizeof(float); + + norm_rope_quant_kernel<<>>( + reinterpret_cast(x_fp16), + reinterpret_cast(weight_fp16), + bias_fp16 ? reinterpret_cast(bias_fp16) : nullptr, + reinterpret_cast(cos_fp32), + reinterpret_cast(sin_fp32), + rstd, + km_fp16 ? reinterpret_cast(km_fp16) : nullptr, + reinterpret_cast(out_int8), + reinterpret_cast(scale_fp32), + B, S, H, Dd, sm_scale); + + if (transient != nullptr) + cudaFreeAsync(transient, stream); + cudaError_t e = cudaGetLastError(); + return (e == cudaSuccess) ? 0 : -5; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cuh b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cuh new file mode 100644 index 00000000..c0c56072 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cuh @@ -0,0 +1,55 @@ +#pragma once +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused RMSNorm + RoPE + per-warp int8 quantization (for Q). +// Input: x [B*S, H*Dd] fp16, in-place layout [B, S, H, Dd]. +// Output: out_int8 [B*S, H*Dd] int8 (same layout as x). +// scale [B, H, num_scale_groups] fp32 +// where num_scale_groups = ceil(S / WARPQ) * (BLKQ / WARPQ) +// For BLKQ=128, WARPQ=32: num_scale_groups = ceil(S/128) * 4 +// Each scale covers WARPQ=32 consecutive tokens for one head. +// rstd_buf: caller-owned scratch of B*S floats for the per-token rstd +// reduction. Pass nullptr to use a stream-ordered transient +// allocation (slower; for one-off / non-hot-path callers). +int fp16_rmsnorm_rope_quant_int8_q( + const void* x_fp16, // [B*S, H*Dd] fp16 input + const void* weight_fp16, // [H*Dd] fp16 norm weight + const void* bias_fp16, // [H*Dd] fp16 Q-proj bias, or nullptr (fused pre-norm) + const void* cos_fp32, // [S, Dd/2] fp32 + const void* sin_fp32, // [S, Dd/2] fp32 + void* out_int8, // [B*S, H*Dd] int8 output + void* scale_fp32, // [B, H, ceil(S/128)*4] fp32 + int B, int S, int H, int Dd, + float eps, float sm_scale, + void* rstd_buf, // [B*S] fp32 caller-owned scratch, or nullptr + cudaStream_t stream); + +// Fused RMSNorm + RoPE + per-block int8 quantization + smooth_k (for K). +// Input: x [B*S, H*Dd] fp16. +// Output: out_int8 [B*S, H*Dd] int8. +// scale [B, H, ceil(S/64)] fp32 +// Each scale covers BLKK=64 consecutive tokens for one head. +// smooth_k: subtract per-head mean (km) before quantization. +// rstd_buf: caller-owned scratch of B*S floats (see _q above); nullptr +// falls back to a stream-ordered transient allocation. +int fp16_rmsnorm_rope_quant_int8_k( + const void* x_fp16, // [B*S, H*Dd] fp16 input + const void* weight_fp16, // [H*Dd] fp16 norm weight + const void* bias_fp16, // [H*Dd] fp16 K-proj bias, or nullptr (fused pre-norm) + const void* cos_fp32, // [S, Dd/2] fp32 + const void* sin_fp32, // [S, Dd/2] fp32 + const void* km_fp16, // [B, 1, H, Dd] fp16 key mean (smooth_k) + void* out_int8, // [B*S, H*Dd] int8 output + void* scale_fp32, // [B, H, ceil(S/64)] fp32 + int B, int S, int H, int Dd, + float eps, float sm_scale, + void* rstd_buf, // [B*S] fp32 caller-owned scratch, or nullptr + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cu b/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cu new file mode 100644 index 00000000..b9f1d657 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cu @@ -0,0 +1,328 @@ +// ================================================================ +// flash_rt — MiniMax-Remover FP8 Conv3d fprop, sm_120a. +// +// Implicit-GEMM FP8 e4m3 conv3d with on-the-fly im2col indexing. +// Tile: BLOCK_M=128, BLOCK_N=128, BLOCK_K=32, 8 warps, cp.async +// 2-stage. Causal temporal cache via virtual dual-pointer concat. +// +// Output: fp16 (NDHWC / channels-last 3D physical layout). +// Bias: fp16 per-channel. +// Alpha: per-output-channel float vector (act_scale × w_scale[co]). +// +// Constraints: Ci % 32 == 0, T_cache == 2, kernel = 3×3×3, pad = 1, +// stride = dilation = 1, groups = 1. +// ================================================================ + +#include "fp8_conv3d_mm_ndhwc_fp16out.cuh" +#include +#include +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +constexpr int MM_BLOCK_M = 128; +constexpr int MM_BLOCK_N = 128; +constexpr int MM_BLOCK_K = 32; +constexpr int MM_N_ATOMS = MM_BLOCK_N / 8; +constexpr int MM_NUM_WARPS = 8; +constexpr int MM_THREADS = MM_NUM_WARPS * 32; +constexpr int MM_STAGES = 2; +constexpr int MM_SMEM_K_STRIDE = 48; + +__device__ __forceinline__ +void mm_mma_m16n8k32_e4m3( + float &d0, float &d1, float &d2, float &d3, + uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, + uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.kind::f8f6f4.m16n8k32.row.col.f32.e4m3.e4m3.f32 " + "{%0, %1, %2, %3}, " + "{%4, %5, %6, %7}, " + "{%8, %9}, " + "{%0, %1, %2, %3};\n" + : "+f"(d0), "+f"(d1), "+f"(d2), "+f"(d3) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), + "r"(b0), "r"(b1)); +} + +// Activation address: virtual concat of cache_x + new_x along T, +// causal output mapping d_in = t_out + kt. +// Spatial pad=1 (h_in/w_in OOB → nullptr → cp.async zeros smem). +__device__ __forceinline__ +const uint8_t* mm_x_byte_ptr(const __nv_fp8_e4m3* cache_x, + const __nv_fp8_e4m3* new_x, + int m_global, int k_global, + int N, int T_cache, int T_new, + int H, int W, int Ci) { + int K_total = 27 * Ci; + int M_total = N * T_new * H * W; + if (k_global >= K_total || m_global >= M_total) return nullptr; + int spatial = T_new * H * W; + int b_idx = m_global / spatial; + int rem = m_global - b_idx * spatial; + int t_out = rem / (H * W); + rem -= t_out * (H * W); + int h_out = rem / W; + int w_out = rem - h_out * W; + int q = k_global / Ci; + int ci0 = k_global % Ci; + int ks = q % 3; q /= 3; + int kr = q % 3; + int kt = q / 3; + int d_in = t_out + kt; + int h_in = h_out + kr - 1; + int w_in = w_out + ks - 1; + if (h_in < 0 || h_in >= H || w_in < 0 || w_in >= W) return nullptr; + if (d_in < T_cache) { + int idx = (((b_idx * T_cache + d_in) * H + h_in) * W + w_in) * Ci + ci0; + return reinterpret_cast(&cache_x[idx]); + } else { + int d_new = d_in - T_cache; + int idx = (((b_idx * T_new + d_new) * H + h_in) * W + w_in) * Ci + ci0; + return reinterpret_cast(&new_x[idx]); + } +} + +// Weight address: [Co, kT, kH, kW, Ci] contiguous. +__device__ __forceinline__ +const uint8_t* mm_w_byte_ptr(const __nv_fp8_e4m3* w, + int co, int k_global, int Co, int Ci) { + int K_total = 27 * Ci; + if (co >= Co || k_global >= K_total) return nullptr; + int q = k_global / Ci; + int ci0 = k_global % Ci; + int ks = q % 3; q /= 3; + int kr = q % 3; + int kt = q / 3; + int idx = (((co * 3 + kt) * 3 + kr) * 3 + ks) * Ci + ci0; + return reinterpret_cast(&w[idx]); +} + +__device__ __forceinline__ +void mm_cp_async_16(uint32_t smem_int_ptr, const uint8_t* src) { + int src_bytes = (src == nullptr) ? 0 : 16; + asm volatile( + "cp.async.ca.shared.global [%0], [%1], 16, %2;\n" + :: "r"(smem_int_ptr), "l"(src), "r"(src_bytes)); +} + +__device__ __forceinline__ +uint32_t mm_to_smem_int(const void* p) { + return static_cast(__cvta_generic_to_shared(p)); +} + +__global__ void __launch_bounds__(MM_THREADS, 2) +fp8_conv3d_mm_kernel( + const __nv_fp8_e4m3* __restrict__ cache_x, + const __nv_fp8_e4m3* __restrict__ new_x, + const __nv_fp8_e4m3* __restrict__ w, + __half* __restrict__ y, + const __half* __restrict__ bias, + const float* __restrict__ alpha_vec, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + int M_tiles, int N_tiles) +{ + __shared__ __align__(16) uint8_t A_smem[MM_STAGES][MM_BLOCK_M * MM_SMEM_K_STRIDE]; + __shared__ __align__(16) uint8_t B_smem[MM_STAGES][MM_BLOCK_N * MM_SMEM_K_STRIDE]; + + const int t = threadIdx.x; + const int warp_id = t / 32; + const int lane = t % 32; + const int l = lane % 4; + const int h = lane / 4; + + const int M_total = N * T_new * H * W; + const int K_total = 27 * Ci; + + const int ld_row_a = t / 2; + const int ld_k_off_a = (t & 1) * 16; + const int ld_row_b = t / 2; + const int ld_k_off_b = (t & 1) * 16; + + { + int tile_idx = blockIdx.x; + int m_idx = tile_idx / N_tiles; + int n_idx = tile_idx % N_tiles; + int m_base = m_idx * MM_BLOCK_M; + int co_base = n_idx * MM_BLOCK_N; + + if (m_base >= M_total || co_base >= Co) return; + + float dA[MM_N_ATOMS] = {0}; + float dB[MM_N_ATOMS] = {0}; + float dC[MM_N_ATOMS] = {0}; + float dD[MM_N_ATOMS] = {0}; + + auto issue_load = [&](int stage, int k_base) { + { + const uint8_t* src = mm_x_byte_ptr(cache_x, new_x, + m_base + ld_row_a, + k_base + ld_k_off_a, + N, T_cache, T_new, H, W, Ci); + uint32_t smem_int = mm_to_smem_int( + &A_smem[stage][ld_row_a * MM_SMEM_K_STRIDE + ld_k_off_a]); + mm_cp_async_16(smem_int, src); + } + { + const uint8_t* src = mm_w_byte_ptr(w, co_base + ld_row_b, + k_base + ld_k_off_b, + Co, Ci); + uint32_t smem_int = mm_to_smem_int( + &B_smem[stage][ld_row_b * MM_SMEM_K_STRIDE + ld_k_off_b]); + mm_cp_async_16(smem_int, src); + } + }; + + // Prologue + issue_load(0, 0); + asm volatile("cp.async.commit_group;\n" ::); + + int compute_stage = 0; + + for (int k_base = 0; k_base < K_total; k_base += MM_BLOCK_K) { + int next_stage = compute_stage ^ 1; + int k_next = k_base + MM_BLOCK_K; + + if (k_next < K_total) { + issue_load(next_stage, k_next); + } + asm volatile("cp.async.commit_group;\n" ::); + asm volatile("cp.async.wait_group 1;\n" ::); + __syncthreads(); + + const int warp_M_off = warp_id * 16; + const int kA0 = 4 * l; + const int kA2 = 4 * l + 16; + + int rA0 = warp_M_off + h; + int rA1 = warp_M_off + h + 8; + uint32_t A0 = *reinterpret_cast( + &A_smem[compute_stage][rA0 * MM_SMEM_K_STRIDE + kA0]); + uint32_t A1 = *reinterpret_cast( + &A_smem[compute_stage][rA1 * MM_SMEM_K_STRIDE + kA0]); + uint32_t A2 = *reinterpret_cast( + &A_smem[compute_stage][rA0 * MM_SMEM_K_STRIDE + kA2]); + uint32_t A3 = *reinterpret_cast( + &A_smem[compute_stage][rA1 * MM_SMEM_K_STRIDE + kA2]); + + #pragma unroll + for (int n_atom = 0; n_atom < MM_N_ATOMS; ++n_atom) { + int co_n = n_atom * 8 + h; + uint32_t B0 = *reinterpret_cast( + &B_smem[compute_stage][co_n * MM_SMEM_K_STRIDE + kA0]); + uint32_t B1 = *reinterpret_cast( + &B_smem[compute_stage][co_n * MM_SMEM_K_STRIDE + kA2]); + mm_mma_m16n8k32_e4m3( + dA[n_atom], dB[n_atom], dC[n_atom], dD[n_atom], + A0, A1, A2, A3, B0, B1); + } + + compute_stage = next_stage; + } + + asm volatile("cp.async.wait_all;\n" ::); + + // Epilogue: per-channel alpha × fp32 accumulator + fp16 bias → fp16 store. + // Output in NDHWC: y[b, t_out, h_out, w_out, co]. + const int warp_M_off = warp_id * 16; + #pragma unroll + for (int n_atom = 0; n_atom < MM_N_ATOMS; ++n_atom) { + int co_pair = co_base + n_atom * 8 + 2 * l; + int row0 = m_base + warp_M_off + h; + int row1 = m_base + warp_M_off + h + 8; + + float a0 = 1.0f, a1 = 1.0f; + if (alpha_vec != nullptr) { + a0 = alpha_vec[co_pair]; + if (co_pair + 1 < Co) a1 = alpha_vec[co_pair + 1]; + } + float b0 = 0.f, b1 = 0.f; + if (bias != nullptr && co_pair < Co) { + b0 = __half2float(bias[co_pair]); + if (co_pair + 1 < Co) b1 = __half2float(bias[co_pair + 1]); + } + + if (co_pair + 1 < Co) { + __half2 packAB; + packAB.x = __float2half_rn(dA[n_atom] * a0 + b0); + packAB.y = __float2half_rn(dB[n_atom] * a1 + b1); + __half2 packCD; + packCD.x = __float2half_rn(dC[n_atom] * a0 + b0); + packCD.y = __float2half_rn(dD[n_atom] * a1 + b1); + if (row0 < M_total) { + *reinterpret_cast<__half2*>(&y[row0 * Co + co_pair]) = packAB; + } + if (row1 < M_total) { + *reinterpret_cast<__half2*>(&y[row1 * Co + co_pair]) = packCD; + } + } else { + auto store = [&](int row, int co, float v, float av, float bv) { + if (row < M_total && co < Co) { + y[row * Co + co] = __float2half_rn(v * av + bv); + } + }; + store(row0, co_pair + 0, dA[n_atom], a0, b0); + store(row0, co_pair + 1, dB[n_atom], a1, b1); + store(row1, co_pair + 0, dC[n_atom], a0, b0); + store(row1, co_pair + 1, dD[n_atom], a1, b1); + } + } + } +} + +int fp8_conv3d_mm_ndhwc_fp16out( + const void* cache_x_fp8, const void* new_x_fp8, + const void* w_fp8, void* y_fp16, + const void* bias_fp16, const void* alpha_vec, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + cudaStream_t stream) +{ + if (Ci % MM_BLOCK_K != 0) { + std::fprintf(stderr, + "[fp8_conv3d_mm] Ci%%%d (got %d) — must be multiple of 32\n", + MM_BLOCK_K, Ci); + return -1; + } + if (T_cache != 2) { + std::fprintf(stderr, + "[fp8_conv3d_mm] T_cache must be 2 (got %d)\n", T_cache); + return -3; + } + if (T_new < 1) { + std::fprintf(stderr, + "[fp8_conv3d_mm] T_new must be >= 1 (got %d)\n", T_new); + return -4; + } + int M = N * T_new * H * W; + int M_tiles = (M + MM_BLOCK_M - 1) / MM_BLOCK_M; + int N_tiles = (Co + MM_BLOCK_N - 1) / MM_BLOCK_N; + int total_tiles = M_tiles * N_tiles; + + dim3 grid(total_tiles); + dim3 block(MM_THREADS); + fp8_conv3d_mm_kernel<<>>( + reinterpret_cast(cache_x_fp8), + reinterpret_cast(new_x_fp8), + reinterpret_cast(w_fp8), + reinterpret_cast<__half*>(y_fp16), + reinterpret_cast(bias_fp16), + reinterpret_cast(alpha_vec), + N, T_cache, T_new, H, W, Ci, Co, + M_tiles, N_tiles); + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "[fp8_conv3d_mm] launch err: %s\n", + cudaGetErrorString(e)); + return -2; + } + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cuh b/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cuh new file mode 100644 index 00000000..ac586df8 --- /dev/null +++ b/csrc/kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cuh @@ -0,0 +1,47 @@ +// ================================================================ +// flash_rt — MiniMax-Remover FP8 Conv3d (implicit-GEMM, NDHWC, +// fp16 output). +// +// Hand-rolled implicit-GEMM FP8 e4m3 conv3d fprop with on-the-fly +// im2col index computation (no intermediate matrix materialization). +// Adapted from the motus v17 kernel with three key changes: +// +// 1. fp16 output (VAE must stay fp16 — no bf16 cast). +// 2. fp16 bias. +// 3. Per-output-channel alpha vector (act_scale × per-channel +// weight_scale) for higher PSNR than per-tensor scaling. +// +// Specialised for 3×3×3 causal conv with stride/dilation/groups = 1, +// spatial pad = 1, temporal causal cache (T_cache = 2, T_new ≥ 1). +// ================================================================ +#pragma once +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +// Fused FP8 e4m3 conv3d fprop (implicit GEMM, NDHWC layout). +// +// cache_x_fp8 : [N, T_cache, H, W, Ci] fp8_e4m3 (may be zero-filled) +// new_x_fp8 : [N, T_new, H, W, Ci] fp8_e4m3 +// w_fp8 : [Co, 3, 3, 3, Ci] fp8_e4m3 +// y_fp16 : [N, T_new, H, W, Co] fp16 (channels-last 3D physical) +// bias_fp16 : [Co] fp16 (may be nullptr) +// alpha_vec : [Co] float (per-channel dequant: act_scale×w_scale[co], +// may be nullptr → alpha = 1.0) +// +// Returns 0 on success, negative on error. +int fp8_conv3d_mm_ndhwc_fp16out( + const void* cache_x_fp8, + const void* new_x_fp8, + const void* w_fp8, + void* y_fp16, + const void* bias_fp16, + const void* alpha_vec, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cu b/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cu new file mode 100644 index 00000000..002eb9ed --- /dev/null +++ b/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cu @@ -0,0 +1,464 @@ +// FlashRT — MiniMax-Remover WanVAE NVFP4 conv3d fprop, sm_120a. +// See nvfp4_conv3d_ndhwc_fp16out.cuh for interface docs. +// +// Adapted from motus_fp4_conv3d_v19sfb_sm120.cu. The compute path (MMA, +// im2col, cp.async pipeline, SF loading) is byte-identical to the motus +// kernel. Only the epilogue changes: fp16 NDHWC output + fp16 bias. +// This eliminates two conversion passes per layer (bf16→fp16 + NCDHW→NDHWC). + +#include "nvfp4_conv3d_ndhwc_fp16out.cuh" +#include +#include +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +namespace { + +constexpr int BLOCK_M = 128; +constexpr int BLOCK_N = 128; +constexpr int BLOCK_K = 64; // FP4 MMA k=64 +constexpr int N_ATOMS = BLOCK_N / 8; // 16 +constexpr int N_GROUPS = N_ATOMS / 4; // 4 +constexpr int NUM_WARPS = 8; +constexpr int THREADS = NUM_WARPS * 32; // 256 +constexpr int STAGES = 2; +constexpr int SMEM_K_STRIDE = 48; // 64/2=32 bytes data + 16 pad (bank conflicts) +constexpr int SF_K_PER_ROW = 4; // 4 UE4M3 bytes per K-tile (1 per 16 elem) + +// ── NVFP4 MMA: m16n8k64 e2m1 × e2m1, block-scaled ── +// Does 4 N-atoms MMAs (scale_vec::4X) with shared A + SFA, varying B + SFB. +__device__ __forceinline__ +void mma_m16n8k64_e2m1_4x( + float &d0, float &d1, float &d2, float &d3, + float &d4, float &d5, float &d6, float &d7, + float &d8, float &d9, float &d10, float &d11, + float &d12, float &d13, float &d14, float &d15, + uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, + uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3, + uint32_t b4, uint32_t b5, uint32_t b6, uint32_t b7, + uint32_t sfa, uint32_t sfb) +{ + constexpr uint16_t bidA = 0, tidA = 0; + constexpr uint16_t tidB0 = 0, tidB1 = 1, tidB2 = 2, tidB3 = 3; + // 4 calls, each m16n8k64, sharing A+SFA, varying B+SFB fragments. + // D layout: {d0,d1,d2,d3} = atom0, {d4,d5,d6,d7} = atom1, etc. + asm volatile( + "mma.sync.aligned.kind::mxf4nvf4.block_scale.scale_vec::4X.m16n8k64" + ".row.col.f32.e2m1.e2m1.f32.ue4m3 " + "{%0,%1,%2,%3},{%4,%5,%6,%7},{%8,%9},{%10,%11,%12,%13}," + "{%14},{%15,%16},{%17},{%18,%19};\n" + : "+f"(d0), "+f"(d1), "+f"(d8), "+f"(d9) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1), + "f"(d0), "f"(d1), "f"(d8), "f"(d9), + "r"(sfa), "h"(bidA), "h"(tidA), + "r"(sfb), "h"(bidA), "h"(tidB0)); + asm volatile( + "mma.sync.aligned.kind::mxf4nvf4.block_scale.scale_vec::4X.m16n8k64" + ".row.col.f32.e2m1.e2m1.f32.ue4m3 " + "{%0,%1,%2,%3},{%4,%5,%6,%7},{%8,%9},{%10,%11,%12,%13}," + "{%14},{%15,%16},{%17},{%18,%19};\n" + : "+f"(d2), "+f"(d3), "+f"(d10), "+f"(d11) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b2), "r"(b3), + "f"(d2), "f"(d3), "f"(d10), "f"(d11), + "r"(sfa), "h"(bidA), "h"(tidA), + "r"(sfb), "h"(bidA), "h"(tidB1)); + asm volatile( + "mma.sync.aligned.kind::mxf4nvf4.block_scale.scale_vec::4X.m16n8k64" + ".row.col.f32.e2m1.e2m1.f32.ue4m3 " + "{%0,%1,%2,%3},{%4,%5,%6,%7},{%8,%9},{%10,%11,%12,%13}," + "{%14},{%15,%16},{%17},{%18,%19};\n" + : "+f"(d4), "+f"(d5), "+f"(d12), "+f"(d13) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b4), "r"(b5), + "f"(d4), "f"(d5), "f"(d12), "f"(d13), + "r"(sfa), "h"(bidA), "h"(tidA), + "r"(sfb), "h"(bidA), "h"(tidB2)); + asm volatile( + "mma.sync.aligned.kind::mxf4nvf4.block_scale.scale_vec::4X.m16n8k64" + ".row.col.f32.e2m1.e2m1.f32.ue4m3 " + "{%0,%1,%2,%3},{%4,%5,%6,%7},{%8,%9},{%10,%11,%12,%13}," + "{%14},{%15,%16},{%17},{%18,%19};\n" + : "+f"(d6), "+f"(d7), "+f"(d14), "+f"(d15) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b6), "r"(b7), + "f"(d6), "f"(d7), "f"(d14), "f"(d15), + "r"(sfa), "h"(bidA), "h"(tidA), + "r"(sfb), "h"(bidA), "h"(tidB3)); +} + +// ── im2col address helpers (FP4 packed, same as motus) ── + +__device__ __forceinline__ +const uint8_t* x_fp4_ptr(const uint8_t* cache_x, const uint8_t* new_x, + int m_global, int k_global, + int N, int T_cache, int T_new, int H, int W, int Ci) { + int K_total = 27 * Ci; + int M_total = N * T_new * H * W; + if (k_global >= K_total || m_global >= M_total) return nullptr; + int spatial = T_new * H * W; + int b_idx = m_global / spatial; + int rem = m_global - b_idx * spatial; + int t_out = rem / (H * W); + rem -= t_out * (H * W); + int h_out = rem / W; + int w_out = rem - h_out * W; + int q = k_global / Ci; + int ci0 = k_global % Ci; + int ks = q % 3; q /= 3; + int kr = q % 3; + int kt = q / 3; + int d_in = t_out + kt; + int h_in = h_out + kr - 1; + int w_in = w_out + ks - 1; + if (h_in < 0 || h_in >= H || w_in < 0 || w_in >= W) return nullptr; + if (d_in < T_cache) { + int idx_elem = (((b_idx * T_cache + d_in) * H + h_in) * W + w_in) * Ci + ci0; + return cache_x + (idx_elem >> 1); + } else { + int d_new = d_in - T_cache; + int idx_elem = (((b_idx * T_new + d_new) * H + h_in) * W + w_in) * Ci + ci0; + return new_x + (idx_elem >> 1); + } +} + +__device__ __forceinline__ +const uint8_t* w_fp4_ptr(const uint8_t* w, int co, int k_global, int Co, int Ci) { + int K_total = 27 * Ci; + if (co >= Co || k_global >= K_total) return nullptr; + int q = k_global / Ci; + int ci0 = k_global % Ci; + int ks = q % 3; q /= 3; + int kr = q % 3; + int kt = q / 3; + int idx_elem = (((co * 3 + kt) * 3 + kr) * 3 + ks) * Ci + ci0; + return w + (idx_elem >> 1); +} + +__device__ __forceinline__ +const uint8_t* sfa_ptr(const uint8_t* cache_sfa, const uint8_t* new_sfa, + int m_global, int k_base, + int N, int T_cache, int T_new, int H, int W, int Ci) { + int Ci_blk = Ci >> 4; + int M_total = N * T_new * H * W; + if (m_global >= M_total) return nullptr; + int spatial = T_new * H * W; + int b_idx = m_global / spatial; + int rem = m_global - b_idx * spatial; + int t_out = rem / (H * W); + rem -= t_out * (H * W); + int h_out = rem / W; + int w_out = rem - h_out * W; + int kt_iter = k_base / Ci; + int ci_block_off = (k_base % Ci) >> 4; + int ks = kt_iter % 3; + int kr = (kt_iter / 3) % 3; + int kt = kt_iter / 9; + int d_in = t_out + kt; + int h_in = h_out + kr - 1; + int w_in = w_out + ks - 1; + if (h_in < 0 || h_in >= H || w_in < 0 || w_in >= W) return nullptr; + if (d_in < T_cache) { + int idx = (((b_idx * T_cache + d_in) * H + h_in) * W + w_in) * Ci_blk + ci_block_off; + return cache_sfa + idx; + } else { + int d_new = d_in - T_cache; + int idx = (((b_idx * T_new + d_new) * H + h_in) * W + w_in) * Ci_blk + ci_block_off; + return new_sfa + idx; + } +} + +__device__ __forceinline__ +const uint8_t* sfb_ptr(const uint8_t* w_sfb, int co, int k_base, int Co, int Ci) { + int Ci_blk = Ci >> 4; + if (co >= Co) return nullptr; + int kt_iter = k_base / Ci; + int ci_block_off = (k_base % Ci) >> 4; + int ks = kt_iter % 3; + int kr = (kt_iter / 3) % 3; + int kt = kt_iter / 9; + int idx = (((co * 3 + kt) * 3 + kr) * 3 + ks) * Ci_blk + ci_block_off; + return w_sfb + idx; +} + +__device__ __forceinline__ +void cp_async_16(uint32_t smem_int, const uint8_t* src) { + int src_bytes = (src == nullptr) ? 0 : 16; + asm volatile("cp.async.ca.shared.global [%0], [%1], 16, %2;\n" + :: "r"(smem_int), "l"(src), "r"(src_bytes)); +} +__device__ __forceinline__ +void cp_async_4(uint32_t smem_int, const uint8_t* src) { + int src_bytes = (src == nullptr) ? 0 : 4; + asm volatile("cp.async.ca.shared.global [%0], [%1], 4, %2;\n" + :: "r"(smem_int), "l"(src), "r"(src_bytes)); +} +__device__ __forceinline__ +uint32_t to_smem_int(const void* p) { + return static_cast(__cvta_generic_to_shared(p)); +} + +// ── Main kernel ── +__global__ void __launch_bounds__(THREADS, 2) +nvfp4_conv3d_kernel( + const uint8_t* __restrict__ cache_x, + const uint8_t* __restrict__ new_x, + const uint8_t* __restrict__ w, + const uint8_t* __restrict__ cache_sfa, + const uint8_t* __restrict__ new_sfa, + const uint8_t* __restrict__ w_sfb, + __half* __restrict__ y, // fp16 NDHWC output + const __half* __restrict__ bias, // fp16 per-channel + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + float alpha, + int M_tiles, int N_tiles) +{ + __shared__ __align__(16) uint8_t A_smem [STAGES][BLOCK_M * SMEM_K_STRIDE]; + __shared__ __align__(16) uint8_t B_smem [STAGES][BLOCK_N * SMEM_K_STRIDE]; + __shared__ __align__(16) uint8_t A_sf_smem[STAGES][BLOCK_M * SF_K_PER_ROW]; + __shared__ __align__(16) uint8_t B_sf_smem[STAGES][BLOCK_N * SF_K_PER_ROW]; + + const int t = threadIdx.x; + const int warp_id = t / 32; + const int lane = t % 32; + const int l = lane % 4; + const int h = lane / 4; + + const int M_total = N * T_new * H * W; + const int K_total = 27 * Ci; + + const int ld_row_a = t / 2; + const int ld_k_off_a = (t & 1) * 32; // 32 FP4 elements = 16 bytes + const int ld_row_b = t / 2; + const int ld_k_off_b = (t & 1) * 32; + + int tile_idx = blockIdx.x; + int m_idx = tile_idx / N_tiles; + int n_idx = tile_idx % N_tiles; + int m_base = m_idx * BLOCK_M; + int co_base = n_idx * BLOCK_N; + if (m_base >= M_total || co_base >= Co) return; + + float dA[N_ATOMS] = {0}; + float dB[N_ATOMS] = {0}; + float dC[N_ATOMS] = {0}; + float dD[N_ATOMS] = {0}; + + auto issue_load = [&](int stage, int k_base) { + // FP4 A + { + const uint8_t* src = x_fp4_ptr(cache_x, new_x, + m_base + ld_row_a, + k_base + ld_k_off_a, + N, T_cache, T_new, H, W, Ci); + uint32_t smem_int = to_smem_int( + &A_smem[stage][ld_row_a * SMEM_K_STRIDE + (ld_k_off_a >> 1)]); + cp_async_16(smem_int, src); + } + // FP4 B + { + const uint8_t* src = w_fp4_ptr(w, co_base + ld_row_b, + k_base + ld_k_off_b, Co, Ci); + uint32_t smem_int = to_smem_int( + &B_smem[stage][ld_row_b * SMEM_K_STRIDE + (ld_k_off_b >> 1)]); + cp_async_16(smem_int, src); + } + // SF: first 128 threads load A_sf, next 128 load B_sf + if (t < BLOCK_M) { + const uint8_t* src = sfa_ptr(cache_sfa, new_sfa, + m_base + t, k_base, + N, T_cache, T_new, H, W, Ci); + uint32_t smem_int = to_smem_int(&A_sf_smem[stage][t * SF_K_PER_ROW]); + cp_async_4(smem_int, src); + } else { + int n_idx_t = t - BLOCK_M; + const uint8_t* src = sfb_ptr(w_sfb, co_base + n_idx_t, k_base, Co, Ci); + uint32_t smem_int = to_smem_int(&B_sf_smem[stage][n_idx_t * SF_K_PER_ROW]); + cp_async_4(smem_int, src); + } + }; + + // Prologue + issue_load(0, 0); + asm volatile("cp.async.commit_group;\n" ::); + + int compute_stage = 0; + + for (int k_base = 0; k_base < K_total; k_base += BLOCK_K) { + int next_stage = compute_stage ^ 1; + int k_next = k_base + BLOCK_K; + if (k_next < K_total) issue_load(next_stage, k_next); + asm volatile("cp.async.commit_group;\n" ::); + asm volatile("cp.async.wait_group 1;\n" ::); + __syncthreads(); + + const int warp_M_off = warp_id * 16; + const int kA0 = 4 * l; + const int kA2 = 4 * l + 16; + + // Load A fragments (4 uint32 per lane) + int rA0 = warp_M_off + h; + int rA1 = warp_M_off + h + 8; + uint32_t A0 = *reinterpret_cast( + &A_smem[compute_stage][rA0 * SMEM_K_STRIDE + kA0]); + uint32_t A1 = *reinterpret_cast( + &A_smem[compute_stage][rA1 * SMEM_K_STRIDE + kA0]); + uint32_t A2 = *reinterpret_cast( + &A_smem[compute_stage][rA0 * SMEM_K_STRIDE + kA2]); + uint32_t A3 = *reinterpret_cast( + &A_smem[compute_stage][rA1 * SMEM_K_STRIDE + kA2]); + + // SFA: 4 UE4M3 bytes packed in 1 uint32 for this lane's M-row + int sfa_m_row; + if ((lane & 3) == 1) { + sfa_m_row = warp_M_off + (lane >> 2) + 8; + } else { + sfa_m_row = warp_M_off + (lane >> 2); + } + uint32_t SFA = *reinterpret_cast( + &A_sf_smem[compute_stage][sfa_m_row * SF_K_PER_ROW]); + + // 4 N-groups, each does 4 atoms (scale_vec::4X) + #pragma unroll + for (int g = 0; g < N_GROUPS; ++g) { + int base = g * 4; + int co0 = (base + 0) * 8 + h; + int co1 = (base + 1) * 8 + h; + int co2 = (base + 2) * 8 + h; + int co3 = (base + 3) * 8 + h; + uint32_t B0 = *reinterpret_cast( + &B_smem[compute_stage][co0 * SMEM_K_STRIDE + kA0]); + uint32_t B1 = *reinterpret_cast( + &B_smem[compute_stage][co0 * SMEM_K_STRIDE + kA2]); + uint32_t B2 = *reinterpret_cast( + &B_smem[compute_stage][co1 * SMEM_K_STRIDE + kA0]); + uint32_t B3 = *reinterpret_cast( + &B_smem[compute_stage][co1 * SMEM_K_STRIDE + kA2]); + uint32_t B4 = *reinterpret_cast( + &B_smem[compute_stage][co2 * SMEM_K_STRIDE + kA0]); + uint32_t B5 = *reinterpret_cast( + &B_smem[compute_stage][co2 * SMEM_K_STRIDE + kA2]); + uint32_t B6 = *reinterpret_cast( + &B_smem[compute_stage][co3 * SMEM_K_STRIDE + kA0]); + uint32_t B7 = *reinterpret_cast( + &B_smem[compute_stage][co3 * SMEM_K_STRIDE + kA2]); + + int sfb_n = g * 32 + l * 8 + h; + uint32_t SFB = *reinterpret_cast( + &B_sf_smem[compute_stage][sfb_n * SF_K_PER_ROW]); + + mma_m16n8k64_e2m1_4x( + dA[base+0], dB[base+0], dA[base+1], dB[base+1], + dA[base+2], dB[base+2], dA[base+3], dB[base+3], + dC[base+0], dD[base+0], dC[base+1], dD[base+1], + dC[base+2], dD[base+2], dC[base+3], dD[base+3], + A0, A1, A2, A3, + B0, B1, B2, B3, B4, B5, B6, B7, + SFA, SFB); + } + compute_stage = next_stage; + } + asm volatile("cp.async.wait_all;\n" ::); + + // ── Epilogue: fp16 NDHWC output ── + // y[b, t_out, h_out, w_out, co] at offset row * Co + co + // (row = m_global = b*T_new*H*W + t_out*H*W + h_out*W + w_out) + const int warp_M_off = warp_id * 16; + #pragma unroll + for (int n_atom = 0; n_atom < N_ATOMS; ++n_atom) { + int co_pair = co_base + n_atom * 8 + 2 * l; + int row0 = m_base + warp_M_off + h; + int row1 = m_base + warp_M_off + h + 8; + + float b0 = 0.f, b1 = 0.f; + if (bias != nullptr && co_pair < Co) { + b0 = __half2float(bias[co_pair]); + if (co_pair + 1 < Co) b1 = __half2float(bias[co_pair + 1]); + } + + if (co_pair + 1 < Co) { + __half2 packAB, packCD; + packAB.x = __float2half_rn(dA[n_atom] * alpha + b0); + packAB.y = __float2half_rn(dB[n_atom] * alpha + b1); + packCD.x = __float2half_rn(dC[n_atom] * alpha + b0); + packCD.y = __float2half_rn(dD[n_atom] * alpha + b1); + if (row0 < M_total) { + *reinterpret_cast<__half2*>(&y[row0 * Co + co_pair]) = packAB; + } + if (row1 < M_total) { + *reinterpret_cast<__half2*>(&y[row1 * Co + co_pair]) = packCD; + } + } else { + auto store = [&](int row, int co, float v, float bv) { + if (row < M_total && co < Co) { + y[row * Co + co] = __float2half_rn(v * alpha + bv); + } + }; + store(row0, co_pair + 0, dA[n_atom], b0); + store(row0, co_pair + 1, dB[n_atom], b1); + store(row1, co_pair + 0, dC[n_atom], b0); + store(row1, co_pair + 1, dD[n_atom], b1); + } + } +} + +} // namespace + +extern "C" int nvfp4_conv3d_ndhwc_fp16out( + const void* cache_x_fp4, + const void* new_x_fp4, + const void* w_fp4, + const void* cache_sfa, + const void* new_sfa, + const void* w_sfb, + void* y_fp16, + const void* bias_fp16, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + float alpha, cudaStream_t stream) +{ + if (Ci % BLOCK_K != 0) { + std::fprintf(stderr, + "[nvfp4_conv3d] Ci%%%d (got %d) — must be multiple of 64\n", + BLOCK_K, Ci); + return -1; + } + if (Co % 8 != 0) { + std::fprintf(stderr, "[nvfp4_conv3d] Co%%8 (got %d)\n", Co); + return -2; + } + if (T_cache != 2) { + std::fprintf(stderr, "[nvfp4_conv3d] T_cache must be 2 (got %d)\n", T_cache); + return -3; + } + int M = N * T_new * H * W; + int M_tiles = (M + BLOCK_M - 1) / BLOCK_M; + int N_tiles = (Co + BLOCK_N - 1) / BLOCK_N; + int total_tiles = M_tiles * N_tiles; + + dim3 grid(total_tiles); + dim3 block(THREADS); + nvfp4_conv3d_kernel<<>>( + reinterpret_cast(cache_x_fp4), + reinterpret_cast(new_x_fp4), + reinterpret_cast(w_fp4), + reinterpret_cast(cache_sfa), + reinterpret_cast(new_sfa), + reinterpret_cast(w_sfb), + reinterpret_cast<__half*>(y_fp16), + reinterpret_cast(bias_fp16), + N, T_cache, T_new, H, W, Ci, Co, alpha, + M_tiles, N_tiles); + cudaError_t e = cudaGetLastError(); + if (e != cudaSuccess) { + std::fprintf(stderr, "[nvfp4_conv3d] launch err: %s\n", + cudaGetErrorString(e)); + return -10; + } + return 0; +} + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cuh b/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cuh new file mode 100644 index 00000000..d7e929e0 --- /dev/null +++ b/csrc/kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cuh @@ -0,0 +1,47 @@ +// FlashRT — MiniMax-Remover WanVAE NVFP4 conv3d fprop, sm_120a. +// +// Purpose-built NVFP4 (W4A4) implicit-GEMM conv3d for the WanVAE. +// Adapted from motus_fp4_conv3d_v19sfb with three WanVAE-specific optimizations: +// +// 1. **fp16 output** (not bf16): WanVAE is fp16-native; eliminates the +// bf16→fp16 conversion that cost ~0.3 ms/layer in the Python wrapper. +// +// 2. **NDHWC output** (not NCDHW): matches the channels-last 3D pipeline; +// eliminates the NCDHW→channels-last format conversion. The downstream +// conv receives data in its preferred layout with zero-copy. +// +// 3. **fp16 bias** (not bf16): matches WanCausalConv3d.bias dtype. +// +// The compute path (MMA, im2col, cp.async pipeline, SF loading) is identical +// to the proven motus kernel. Only the epilogue changes. +// +// Tile: BLOCK_M=128, BLOCK_N=128, BLOCK_K=64, 8 warps, cp.async 2-stage. +// MMA: mma.sync.kind::mxf4nvf4.block_scale.scale_vec::4X.m16n8k64 +// +// Constraints: Ci % 64 == 0, T_cache == 2, kernel = 3×3×3, pad = 1. +// (WanVAE Ci=192/384 qualify; Ci=96 stays on FP8.) +#pragma once + +#include +#include +#include + +namespace flash_rt { +namespace kernels { +namespace minimax_remover { + +extern "C" int nvfp4_conv3d_ndhwc_fp16out( + const void* cache_x_fp4, // [B,T_cache,H,W,Ci/2] uint8 packed FP4 + const void* new_x_fp4, // [B,T_new,H,W,Ci/2] uint8 packed FP4 + const void* w_fp4, // [Co,3,3,3,Ci/2] uint8 packed FP4 + const void* cache_sfa, // [B,T_cache,H,W,Ci/16] uint8 UE4M3 + const void* new_sfa, // [B,T_new,H,W,Ci/16] uint8 UE4M3 + const void* w_sfb, // [Co,3,3,3,Ci/16] uint8 UE4M3 + void* y_fp16, // [B,T_new,H,W,Co] __half NDHWC output + const void* bias_fp16, // [Co] __half, or nullptr + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + float alpha, cudaStream_t stream); + +} // namespace minimax_remover +} // namespace kernels +} // namespace flash_rt diff --git a/csrc/minimax_remover_extra_bindings.cpp b/csrc/minimax_remover_extra_bindings.cpp new file mode 100644 index 00000000..8823621f --- /dev/null +++ b/csrc/minimax_remover_extra_bindings.cpp @@ -0,0 +1,553 @@ +// ================================================================ +// flash_rt_minimax_remover — standalone pybind module for +// MiniMax-Remover VAE-specific fused fp16 kernels. +// +// Kept separate from flash_rt_kernels so they can be added/rebuilt +// independently without touching the main bindings. Build with: +// cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ... +// +// Kernels: fp16_rms_norm_ncdhw, fp16_rms_silu_ncdhw +// ================================================================ +#include +#include +#include +#include + +#include "kernels/minimax_remover/fp16_rms_norm_ncdhw.cuh" +#include "kernels/minimax_remover/fp16_rms_silu_ncdhw.cuh" +#include "kernels/minimax_remover/fp16_rms_norm_ndhwc.cuh" +#include "kernels/minimax_remover/fp8_conv3d_mm_ndhwc_fp16out.cuh" +#include "kernels/minimax_remover/fp16_quant_fp8_per_tensor.cuh" +#include "kernels/minimax_remover/fp16_rms_silu_fp8_ndhwc.cuh" +#include "kernels/minimax_remover/fp16_bias_gelu_quant_fp8.cuh" +#include "kernels/minimax_remover/fp16_bias_gate_residual.cuh" +#include "kernels/minimax_remover/fp16_ada_layernorm_quant_fp8.cuh" +#include "kernels/minimax_remover/fp16_rmsnorm_rope.cuh" +#include "kernels/minimax_remover/fp16_rmsnorm_rope_quant_int8.cuh" +#include "kernels/minimax_remover/fp16_quant_nvfp4_ndhwc.cuh" +#include "kernels/minimax_remover/nvfp4_conv3d_ndhwc_fp16out.cuh" + +namespace py = pybind11; + +static inline void* to_ptr(uintptr_t p) { return reinterpret_cast(p); } +static inline cudaStream_t to_stream(uintptr_t s) { + return reinterpret_cast(s); +} + +PYBIND11_MODULE(flash_rt_minimax_remover, m) { + m.doc() = "MiniMax-Remover VAE fused fp16 kernels"; + + m.def("fp16_rms_norm_ncdhw", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp16, int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_rms_norm_ncdhw( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp16), B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp16"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 NCDHW RMSNorm (fp16 in/out, fp32 stats, no cast). " + "Replaces WanRMS_norm.forward (4 full-tensor fp32 passes)."); + + m.def("fp16_rms_silu_ncdhw", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp16, int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_rms_silu_ncdhw( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp16), B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp16"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 NCDHW RMSNorm + SiLU (fp16 in/out, fp32 stats+act, " + "no cast). Replaces norm->silu two-pass in WanResidualBlock."); + + // ── Channels-last (NDHWC) norm kernels ── + m.def("fp16_rms_norm_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp16, int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_rms_norm_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp16), B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp16"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 channels-last (NDHWC) RMSNorm. C values contiguous, " + "eliminates nchw<->nhwc format conversion for cuDNN conv3d."); + + m.def("fp16_rms_silu_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp16, int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_rms_silu_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp16), B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp16"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 channels-last (NDHWC) RMSNorm + SiLU."); + + // ── FP8 implicit-GEMM conv3d (3×3×3 causal, NDHWC, fp16 output) ── + m.def("fp8_conv3d_mm_ndhwc_fp16out", + [](uintptr_t cache_x_fp8, uintptr_t new_x_fp8, + uintptr_t w_fp8, uintptr_t y_fp16, + uintptr_t bias_fp16, uintptr_t alpha_vec, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp8_conv3d_mm_ndhwc_fp16out( + to_ptr(cache_x_fp8), to_ptr(new_x_fp8), + to_ptr(w_fp8), to_ptr(y_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + alpha_vec ? to_ptr(alpha_vec) : nullptr, + N, T_cache, T_new, H, W, Ci, Co, + to_stream(stream)); + }, + py::arg("cache_x_fp8"), py::arg("new_x_fp8"), + py::arg("w_fp8"), py::arg("y_fp16"), + py::arg("bias_fp16"), py::arg("alpha_vec"), + py::arg("N"), py::arg("T_cache"), py::arg("T_new"), + py::arg("H"), py::arg("W"), py::arg("Ci"), py::arg("Co"), + py::arg("stream") = 0, + "FP8 e4m3 implicit-GEMM conv3d fprop (3x3x3 causal, NDHWC, " + "fp16 output). Per-channel alpha vector [Co] float and fp16 " + "bias [Co]. No im2col materialization."); + + // ── Fused fp16→fp8 per-tensor quantize (2-pass, no host sync) ── + m.def("fp16_quant_fp8_per_tensor", + [](uintptr_t x_fp16, uintptr_t y_fp8, + uintptr_t scale_out, uintptr_t amax_buf, + int n, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_quant_fp8_per_tensor( + to_ptr(x_fp16), to_ptr(y_fp8), + to_ptr(scale_out), to_ptr(amax_buf), + n, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("y_fp8"), + py::arg("scale_out"), py::arg("amax_buf"), + py::arg("n"), py::arg("stream") = 0, + "Fused per-tensor fp16→fp8 e4m3 quantize (amax + scale on " + "device, no host sync). Writes float scale to scale_out."); + + m.def("amax_fp16", + [](uintptr_t x_fp16, uintptr_t amax_buf, + int n, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::amax_fp16( + to_ptr(x_fp16), to_ptr(amax_buf), n, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("amax_buf"), + py::arg("n"), py::arg("stream") = 0, + "Grid-stride amax reduction into amax_buf via atomicMax. " + "Caller must zero amax_buf before first call. Multiple calls " + "accumulate (for multi-tensor shared-scale quantization)."); + + m.def("quantize_fp16_fp8_with_amax", + [](uintptr_t x_fp16, uintptr_t y_fp8, + uintptr_t amax_buf, uintptr_t scale_out, + int n, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + quantize_fp16_fp8_with_amax( + to_ptr(x_fp16), to_ptr(y_fp8), + to_ptr(amax_buf), to_ptr(scale_out), + n, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("y_fp8"), + py::arg("amax_buf"), py::arg("scale_out"), + py::arg("n"), py::arg("stream") = 0, + "Quantize fp16→fp8 using pre-computed amax in amax_buf. " + "Writes float scale to scale_out."); + + // ── Dual quantize: two buffers, one shared amax, one launch ── + m.def("quantize_fp16_fp8_with_amax_dual", + [](uintptr_t x1_fp16, uintptr_t y1_fp8, int n1, + uintptr_t x2_fp16, uintptr_t y2_fp8, int n2, + uintptr_t amax_buf, uintptr_t scale_out, + uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + quantize_fp16_fp8_with_amax_dual( + to_ptr(x1_fp16), to_ptr(y1_fp8), n1, + to_ptr(x2_fp16), to_ptr(y2_fp8), n2, + to_ptr(amax_buf), + scale_out ? to_ptr(scale_out) : nullptr, + to_stream(stream)); + }, + py::arg("x1_fp16"), py::arg("y1_fp8"), py::arg("n1"), + py::arg("x2_fp16"), py::arg("y2_fp8"), py::arg("n2"), + py::arg("amax_buf"), py::arg("scale_out") = 0, + py::arg("stream") = 0, + "Dual quantize: two fp16 buffers → fp8 with shared amax in " + "one kernel launch. Saves one launch vs two separate calls."); + + // ── Fused norm+silu+amax / norm+silu+quant_fp8 (NDHWC) ── + m.def("fp16_rms_silu_amax_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp16, uintptr_t amax_buf, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_amax_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp16), to_ptr(amax_buf), + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp16"), py::arg("amax_buf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 NDHWC RMSNorm+SiLU+amax. Writes fp16 output and " + "accumulates |output| into amax_buf via atomicMax (caller must " + "zero amax_buf before first call). Saves one full read of y " + "vs separate norm+silu then amax."); + + m.def("fp16_rms_silu_quant_fp8_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp8, uintptr_t amax_buf, uintptr_t scale_out, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_quant_fp8_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp8), to_ptr(amax_buf), + scale_out ? to_ptr(scale_out) : nullptr, + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp8"), py::arg("amax_buf"), py::arg("scale_out") = 0, + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused FP16 NDHWC RMSNorm+SiLU → FP8 e4m3 quantize. Reads " + "pre-computed amax from device. Does NOT write fp16 output — " + "eliminates the fp16 intermediate between norm and conv."); + + m.def("fp16_rms_silu_amax_quant_fp8_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp8, uintptr_t scale_out, uintptr_t amax_buf, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_amax_quant_fp8_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp8), to_ptr(scale_out), to_ptr(amax_buf), + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp8"), py::arg("scale_out"), py::arg("amax_buf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "2-pass fused norm+silu+amax+quant → FP8. Pass 1 computes amax " + "(no write); pass 2 re-reads x and quantizes. Produces ONLY fp8 " + "output + scale, no fp16 intermediate."); + + m.def("fp16_rms_silu_amax_quant_fp8_ndhwc_nozero", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp8, uintptr_t scale_out, uintptr_t amax_buf, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_amax_quant_fp8_ndhwc_nozero( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp8), to_ptr(scale_out), to_ptr(amax_buf), + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp8"), py::arg("scale_out"), py::arg("amax_buf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Same as fp16_rms_silu_amax_quant_fp8_ndhwc but does NOT zero " + "amax_buf before pass 1. For running-max mode: caller seeds " + "amax_buf with historical max; pass 1 accumulates current output; " + "pass 2 quantizes with max(historical, current)."); + + // ── Fused FFN epilogue: bias + gelu + quant → fp8 (transformer) ── + m.def("bias_gelu_quant_fp16_fp8", + [](uintptr_t gemm_out, uintptr_t bias, uintptr_t out, + uintptr_t d_scale, int M, int N, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + bias_gelu_quant_fp16_fp8( + to_ptr(gemm_out), to_ptr(bias), to_ptr(out), + reinterpret_cast(to_ptr(d_scale)), + M, N, to_stream(stream)); + }, + py::arg("gemm_out"), py::arg("bias"), py::arg("out"), + py::arg("d_scale"), py::arg("M"), py::arg("N"), + py::arg("stream") = 0, + "Fused FFN epilogue: fp16 GEMM-out + bias → tanh-gelu → fp8 e4m3. " + "Replaces add_bias_fp16 + gelu_inplace_fp16 + quantize_fp8 (3 " + "kernels → 1). Output is the pre-quantised input of the next FP8 " + "Linear, which skips its own activation quantise."); + + m.def("bias_quant_fp16_fp8", + [](uintptr_t gemm_out, uintptr_t bias, uintptr_t out, + uintptr_t d_scale, int M, int N, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + bias_quant_fp16_fp8( + to_ptr(gemm_out), to_ptr(bias), to_ptr(out), + reinterpret_cast(to_ptr(d_scale)), + M, N, to_stream(stream)); + }, + py::arg("gemm_out"), py::arg("bias"), py::arg("out"), + py::arg("d_scale"), py::arg("M"), py::arg("N"), + py::arg("stream") = 0, + "Fused: fp16 GEMM-out + bias → fp8 e4m3 (identity activation). " + "For Linear→Linear chains with no activation in between."); + + // ── Fused bias + gate·residual (eliminates the mid-block bias-add RMW) ── + m.def("fp16_bias_gate_residual_bcast", + [](uintptr_t out_fp16, uintptr_t bias_fp16, uintptr_t gate_fp16, + uintptr_t residual_fp16, int M, int D, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_bias_gate_residual_bcast( + to_ptr(out_fp16), to_ptr(bias_fp16), to_ptr(gate_fp16), + to_ptr(residual_fp16), M, D, to_stream(stream)); + }, + py::arg("out_fp16"), py::arg("bias_fp16"), + py::arg("gate_fp16"), py::arg("residual_fp16"), + py::arg("M"), py::arg("D"), + py::arg("stream") = 0, + "Fused: residual[m,d] += (out[m,d] + bias[d]) * gate[d]. " + "Replaces add_bias_fp16 + gate_mul_residual_bcast (2 kernels → 1) " + "for the O-proj and FFN-down slots — cuts one full [M,D] fp16 " + "read-modify-write per call. D must be a multiple of 8."); + + m.def("fp16_add_bias_vec8", + [](uintptr_t x_fp16, uintptr_t bias_fp16, + int M, int D, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_add_bias_vec8( + to_ptr(x_fp16), to_ptr(bias_fp16), M, D, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("bias_fp16"), + py::arg("M"), py::arg("D"), + py::arg("stream") = 0, + "Vectorised (fp16x8) in-place add_bias: x[m,d] += bias[d]. " + "Replaces the scalar decoder_fused add_bias_fp16 kernel for the " + "Q/K/V projections; ~8× fewer memory transactions. D must be " + "a multiple of 8."); + + m.def("fp16_ada_layernorm_quant_fp8", + [](uintptr_t x_fp16, uintptr_t scale_fp32, uintptr_t shift_fp32, + uintptr_t act_scale_fp32, uintptr_t out_fp8, + int S, int D, float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_ada_layernorm_quant_fp8( + to_ptr(x_fp16), to_ptr(scale_fp32), to_ptr(shift_fp32), + to_ptr(act_scale_fp32), to_ptr(out_fp8), + S, D, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("scale_fp32"), py::arg("shift_fp32"), + py::arg("act_scale_fp32"), py::arg("out_fp8"), + py::arg("S"), py::arg("D"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused fp32-stat LayerNorm + adaLN modulation + per-tensor fp8 " + "e4m3 quantise. Reads x[S,D] fp16, applies " + "y = (x-mean)/std * (1+scale[d]) + shift[d], then divides by the " + "target Linear's static act_scale and casts to fp8_e4m3fn. " + "Replaces the 3-kernel ada_layernorm_fp16_io + quantize_fp8 + " + "gemm-descale sequence with a single pass — eliminates the " + "intermediate fp16 read of the LayerNorm output. D must be a " + "multiple of 8."); + + m.def("fp16_rmsnorm_rope_bshd", + [](uintptr_t x_fp16, uintptr_t weight_fp16, + uintptr_t cos_fp32, uintptr_t sin_fp32, + int B, int S, int H, int Dd, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_rmsnorm_rope_bshd( + to_ptr(x_fp16), to_ptr(weight_fp16), + to_ptr(cos_fp32), to_ptr(sin_fp32), + B, S, H, Dd, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("weight_fp16"), + py::arg("cos_fp32"), py::arg("sin_fp32"), + py::arg("B"), py::arg("S"), py::arg("H"), py::arg("Dd"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused per-token RMSNorm (fp32 stats, fp16 affine) + interleaved " + "RoPE on the native [B,S,H,Dd] fp16 layout. RMS reduction is " + "across the full D = H*Dd (matches qk_norm='rms_norm_across_heads'). " + "Replaces the 2-kernel rms_norm_fp32stat + rope_apply_bshd Triton " + "sequence with one pass — eliminates one full fp16 read+write of " + "the Q/K tensor. Dd must be a multiple of 8."); + + // ── Fused RMSNorm + RoPE + int8 quantize (Q, per-warp) ────────── + m.def("fp16_rmsnorm_rope_quant_int8_q", + [](uintptr_t x_fp16, uintptr_t weight_fp16, + uintptr_t bias_fp16, + uintptr_t cos_fp32, uintptr_t sin_fp32, + uintptr_t out_int8, uintptr_t scale_fp32, + int B, int S, int H, int Dd, + float eps, float sm_scale, + uintptr_t rstd_buf, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rmsnorm_rope_quant_int8_q( + to_ptr(x_fp16), to_ptr(weight_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(cos_fp32), to_ptr(sin_fp32), + to_ptr(out_int8), to_ptr(scale_fp32), + B, S, H, Dd, eps, sm_scale, + rstd_buf ? to_ptr(rstd_buf) : nullptr, + to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("weight_fp16"), + py::arg("bias_fp16") = 0, + py::arg("cos_fp32"), py::arg("sin_fp32"), + py::arg("out_int8"), py::arg("scale_fp32"), + py::arg("B"), py::arg("S"), py::arg("H"), py::arg("Dd"), + py::arg("eps") = 1e-6f, py::arg("sm_scale") = 1.0f, + py::arg("rstd_buf") = 0, py::arg("stream") = 0, + "Fused RMSNorm + RoPE + per-warp int8 quantization for Q. " + "Eliminates the fp16 intermediate between norm+rope and quantize. " + "bias_fp16 (Q-proj bias) is added pre-norm (fused: replaces the " + "separate add_bias kernel); pass 0 to skip. " + "rstd_buf: caller-owned [B*S] fp32 scratch (reused across calls to " + "avoid hot-path allocation); pass 0 for a transient allocation. " + "Output: int8 [B*S, H*Dd], scale [B, H, ceil(S/32)]. " + "sm_scale is folded into quantization (softmax scale pre-multiply)."); + + // ── Fused RMSNorm + RoPE + int8 quantize (K, per-block + smooth_k) ─ + m.def("fp16_rmsnorm_rope_quant_int8_k", + [](uintptr_t x_fp16, uintptr_t weight_fp16, + uintptr_t bias_fp16, + uintptr_t cos_fp32, uintptr_t sin_fp32, + uintptr_t km_fp16, + uintptr_t out_int8, uintptr_t scale_fp32, + int B, int S, int H, int Dd, + float eps, float sm_scale, + uintptr_t rstd_buf, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rmsnorm_rope_quant_int8_k( + to_ptr(x_fp16), to_ptr(weight_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(cos_fp32), to_ptr(sin_fp32), + km_fp16 ? to_ptr(km_fp16) : nullptr, + to_ptr(out_int8), to_ptr(scale_fp32), + B, S, H, Dd, eps, sm_scale, + rstd_buf ? to_ptr(rstd_buf) : nullptr, + to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("weight_fp16"), + py::arg("bias_fp16") = 0, + py::arg("cos_fp32"), py::arg("sin_fp32"), + py::arg("km_fp16"), + py::arg("out_int8"), py::arg("scale_fp32"), + py::arg("B"), py::arg("S"), py::arg("H"), py::arg("Dd"), + py::arg("eps") = 1e-6f, py::arg("sm_scale") = 1.0f, + py::arg("rstd_buf") = 0, py::arg("stream") = 0, + "Fused RMSNorm + RoPE + per-block int8 quantization for K with " + "smooth_k (subtract key mean). Eliminates fp16 intermediate. " + "bias_fp16 (K-proj bias) added pre-norm (fused); pass 0 to skip. " + "rstd_buf: caller-owned [B*S] fp32 scratch (reused across calls); " + "pass 0 for a transient allocation. " + "Output: int8 [B*S, H*Dd], scale [B, H, ceil(S/64)]. " + "km_fp16 can be 0 (nullptr) to skip smooth_k."); + + // ── NVFP4 fused quantization kernels (WanVAE FP4 path) ── + + m.def("fp16_rms_silu_quant_nvfp4_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp4, uintptr_t y_sf, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_quant_nvfp4_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp4), to_ptr(y_sf), + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp4"), py::arg("y_sf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused fp16 NCDHW → RMS_norm + SiLU + NVFP4 quant (NDHWC out). " + "Output: fp4 [B,T,H,W,C/2] uint8, sf [B,T,H,W,C/16] uint8 UE4M3. " + "Requires C%96==0 (WanVAE channels 96/192/384). " + "Eliminates 3 separate passes into one kernel."); + + m.def("fp16_quant_nvfp4_ndhwc", + [](uintptr_t x_fp16, uintptr_t y_fp4, uintptr_t y_sf, + int B, int C, int T, int H, int W, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_quant_nvfp4_ndhwc( + to_ptr(x_fp16), to_ptr(y_fp4), to_ptr(y_sf), + B, C, T, H, W, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("y_fp4"), py::arg("y_sf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("stream") = 0, + "Plain fp16 NCDHW → NVFP4 quant (NDHWC out), no norm/silu. " + "Used for causal-conv cache quantization."); + + m.def("fp16_quant_nvfp4_cl_ndhwc", + [](uintptr_t x_fp16, uintptr_t y_fp4, uintptr_t y_sf, + int B, int C, int T, int H, int W, uintptr_t stream) { + return flash_rt::kernels::minimax_remover::fp16_quant_nvfp4_cl_ndhwc( + to_ptr(x_fp16), to_ptr(y_fp4), to_ptr(y_sf), + B, C, T, H, W, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("y_fp4"), py::arg("y_sf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("stream") = 0, + "fp16 channels-last 3D → NVFP4 quant (NDHWC out). " + "Eliminates contiguous() copy for channels-last inputs."); + + m.def("fp16_rms_silu_quant_nvfp4_cl_ndhwc", + [](uintptr_t x_fp16, uintptr_t gamma_fp16, uintptr_t bias_fp16, + uintptr_t y_fp4, uintptr_t y_sf, + int B, int C, int T, int H, int W, + float eps, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + fp16_rms_silu_quant_nvfp4_cl_ndhwc( + to_ptr(x_fp16), to_ptr(gamma_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + to_ptr(y_fp4), to_ptr(y_sf), + B, C, T, H, W, eps, to_stream(stream)); + }, + py::arg("x_fp16"), py::arg("gamma_fp16"), py::arg("bias_fp16"), + py::arg("y_fp4"), py::arg("y_sf"), + py::arg("B"), py::arg("C"), py::arg("T"), py::arg("H"), py::arg("W"), + py::arg("eps") = 1e-6f, py::arg("stream") = 0, + "Fused fp16 channels-last → RMS_norm + SiLU + NVFP4 quant (NDHWC out)."); + + // ── WanVAE NVFP4 conv3d (purpose-built, fp16 NDHWC output) ── + + m.def("nvfp4_conv3d_ndhwc_fp16out", + [](uintptr_t cache_x_fp4, uintptr_t new_x_fp4, uintptr_t w_fp4, + uintptr_t cache_sfa, uintptr_t new_sfa, uintptr_t w_sfb, + uintptr_t y_fp16, uintptr_t bias_fp16, + int N, int T_cache, int T_new, int H, int W, int Ci, int Co, + float alpha, uintptr_t stream) { + return flash_rt::kernels::minimax_remover:: + nvfp4_conv3d_ndhwc_fp16out( + to_ptr(cache_x_fp4), to_ptr(new_x_fp4), to_ptr(w_fp4), + to_ptr(cache_sfa), to_ptr(new_sfa), to_ptr(w_sfb), + to_ptr(y_fp16), + bias_fp16 ? to_ptr(bias_fp16) : nullptr, + N, T_cache, T_new, H, W, Ci, Co, alpha, to_stream(stream)); + }, + py::arg("cache_x_fp4"), py::arg("new_x_fp4"), py::arg("w_fp4"), + py::arg("cache_sfa"), py::arg("new_sfa"), py::arg("w_sfb"), + py::arg("y_fp16"), py::arg("bias_fp16"), + py::arg("N"), py::arg("T_cache"), py::arg("T_new"), + py::arg("H"), py::arg("W"), py::arg("Ci"), py::arg("Co"), + py::arg("alpha") = 1.0f, py::arg("stream") = 0, + "WanVAE NVFP4 W4A4 conv3d (3x3x3 causal, fp16 NDHWC output). " + "Purpose-built: eliminates bf16→fp16 + NCDHW→NDHWC conversions. " + "Requires Ci%64==0, T_cache==2."); +} diff --git a/docs/minimax_remover_usage.md b/docs/minimax_remover_usage.md index 9a3182eb..944c5132 100644 --- a/docs/minimax_remover_usage.md +++ b/docs/minimax_remover_usage.md @@ -7,27 +7,44 @@ kernelized inference on Blackwell SM120. Two precision paths ship under | Entrypoint | Precision | Recommended use | |------------|-----------|-----------------| | `MiniMaxRemoverPipelineFP8` | FP8 (W8A8) | **Default.** Full-frame inpainting; end-to-end cosine >= 0.999, PSNR ~35-41 dB vs fp16 (see Performance). | -| `MiniMaxRemoverPipeline` | NVFP4 (W4A4) | Cropped small regions only. Full-frame large latents drift/blacken due to FP4 error accumulation. | - -Both reuse the **generic** FlashRT kernels — the package ships **no -model-specific CUDA operators** — and rewrite the transformer Linears as -quantized GEMMs, fuse norm/gate/residual/gelu ops, and use kernel attention -(FA2 / SageAttention). The NVFP4 path additionally captures the N-step -flow-matching loop as a single CUDA Graph; the FP8 path is graph-compatible -(stable static scales, no host sync in steady state) but does not itself -capture a graph. +| `MiniMaxRemoverPipelineFP8` + NVFP4 VAE | FP8 transformer + NVFP4 VAE (W4A4) | **Default (enabled automatically).** Purpose-built NVFP4 conv3d kernel accelerates the VAE encode/decode by ~16%; PSNR ~35 dB vs fp16 (see Performance). | +| `MiniMaxRemoverPipeline` | NVFP4 (W4A4) transformer | Cropped small regions only. Full-frame large latents drift/blacken due to FP4 error accumulation in the transformer denoise loop. | + +Both reuse the **generic** FlashRT kernels for the transformer denoise path +(quantized GEMMs, fused norm/gate/residual/gelu ops, kernel attention via +FA2 / SageAttention). The VAE encode/decode is additionally accelerated by +**model-specific fp16 fused CUDA kernels** in the standalone +`flash_rt_minimax_remover` module (opt-in build, see [Build](#build)): +`fp16_rms_norm_ncdhw` (single-pass RMSNorm, fp16-native), +`fp16_rms_silu_ncdhw` (fused RMSNorm + SiLU), and their channels-last +(NDHWC) variants `fp16_rms_norm_ndhwc` / `fp16_rms_silu_ndhwc` which +keep the entire VAE pipeline in channels-last 3D memory format — +eliminating cuDNN's per-conv `nchw↔nhwc` conversion kernels. The FP8 transformer +path is graph-compatible (stable static scales, no host sync in steady state) +but does not itself capture a graph. The NVFP4 transformer path additionally +captures the N-step flow-matching loop as a single CUDA Graph. + +On top of the FP8 transformer path, the **NVFP4 VAE** optimization +(`install_vae_nvfp4`) replaces eligible 3×3×3 conv3d layers in the WanVAE +with a **purpose-built NVFP4 W4A4 conv3d kernel** (`nvfp4_conv3d_ndhwc_fp16out`) +that uses the SM120 `mma.sync.kind::mxf4nvf4` FP4 MMA for 2× tensor-core +throughput. Unlike the NVFP4 transformer path (which is broken on full-frame +latents), the NVFP4 VAE path works correctly on full-frame inputs because +the VAE is a single-pass encoder/decoder (no iterative error accumulation). +A rolling 2-frame FP4 cache eliminates per-call cache re-quantization. ## Build ```bash cd FlashRT -cmake -S . -B build -DGPU_ARCH=120 -DCMAKE_BUILD_TYPE=Release -cmake --build build -j --target flash_rt_kernels +cmake -S . -B build -DGPU_ARCH=120 -DCMAKE_BUILD_TYPE=Release \ + -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON +cmake --build build -j --target flash_rt_kernels flash_rt_minimax_remover pip install -e ".[torch,minimax-remover]" ``` -`GPU_ARCH=120` (RTX 5090) or `121` selects the Blackwell target; the NVFP4 -surface is compiled in automatically (internally gated by +`GPU_ARCH=120` (RTX 5090 / 5060 Ti) or `121` selects the Blackwell target; +the NVFP4 surface is compiled in automatically (internally gated by `ENABLE_CUTLASS_SM120_NVFP4_W4A16`, which is set from `GPU_ARCH`, not a flag users pass). The FP8 symbols are part of the default build. Then install the runtime extras: @@ -36,6 +53,181 @@ runtime extras: pip install -e ".[minimax-remover]" # diffusers + einops + scipy + sageattention ``` +### VAE fused kernels (standalone module, opt-in) + +The fp16-native fused VAE kernels ship in a **separate** pybind module +`flash_rt_minimax_remover`, following the same opt-in pattern as +`flash_rt_omnivoice`. They are **not** compiled into the default +`flash_rt_kernels` target — enable explicitly: + +```bash +cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ... +cmake --build build -j --target flash_rt_minimax_remover +``` + +| Kernel | Module | Replaces | Effect | +|--------|--------|----------|--------| +| `fp16_rms_norm_ncdhw` | `flash_rt_minimax_remover` | `WanRMS_norm.forward` (4 full-tensor fp32 passes) in attention blocks | ~6x per-call; fp16 in/out, fp32 stats, **no dtype cast** | +| `fp16_rms_silu_ncdhw` | `flash_rt_minimax_remover` | `WanRMS_norm` + `F.silu` two-pass in every `WanResidualBlock` | fused single-pass; eliminates intermediate tensor R/W | +| `fp16_rms_norm_ndhwc` | `flash_rt_minimax_remover` | channels-last (NDHWC) variant of the above for attention-block norm | keeps pipeline in CL → eliminates nchw↔nhwc conversion | +| `fp16_rms_silu_ndhwc` | `flash_rt_minimax_remover` | channels-last fused norm+silu for residual blocks | CL-native, contiguous C reads → faster than NCDHW variant | +| `fp16_rms_silu_amax_ndhwc` | `flash_rt_minimax_remover` | channels-last fused norm+silu+**amax** | fuses amax into the norm+silu pass; saves 1 full read of the activation tensor per conv layer | +| `fp16_rms_silu_quant_fp8_ndhwc` | `flash_rt_minimax_remover` | channels-last fused norm+silu→**FP8 e4m3** quantize | eliminates the fp16 intermediate between norm and conv (reads pre-computed amax) | +| `fp16_rms_silu_amax_quant_fp8_ndhwc` | `flash_rt_minimax_remover` | 2-pass norm+silu+amax+quant→FP8 | combined launcher: pass 1 computes amax (no write), pass 2 quantizes; produces ONLY fp8 + scale | +| `fp8_conv3d_mm_ndhwc_fp16out` | `flash_rt_minimax_remover` | cuDNN fp16 conv3d for applicable 3×3×3 causal convs | FP8 e4m3 implicit-GEMM (no im2col materialization); per-channel weight dequant; fp16 in/out | +| `amax_fp16` + `quantize_fp16_fp8_with_amax` | `flash_rt_minimax_remover` | PyTorch multi-pass `abs().max()` + scale + cast for activation quantization | fused 2-pass device-side amax + quantize; no host sync; shared scale across cache+new frames | +| `quantize_fp16_fp8_with_amax_dual` | `flash_rt_minimax_remover` | two separate `quantize_fp16_fp8_with_amax` calls (cache + new) | single kernel launch quantizes two buffers with shared amax; saves 1 launch per conv layer | +| `bias_gelu_quant_fp16_fp8` | `flash_rt_minimax_remover` | transformer FFN `add_bias_fp16` + `gelu_inplace_fp16` + `quantize_fp8_static_fp16` (3 kernels) | fused single-pass: fp16 GEMM-out + bias → tanh-gelu → fp8 e4m3; output is the pre-quantised input of the next FP8 Linear (skips its activation quantise); eliminates 3 full-tensor fp16 round-trips per FFN block | +| `bias_quant_fp16_fp8` | `flash_rt_minimax_remover` | `add_bias_fp16` + `quantize_fp8_static_fp16` (identity activation variant) | fused bias + quant for Linear→Linear chains with no activation | +| `fp16_bias_gate_residual_bcast` | `flash_rt_minimax_remover` | transformer O-proj / FFN-down `add_bias_fp16` + `gate_mul_residual_bcast` (2 kernels) | fused single-pass fp16x8 (uint4) kernel: `residual[m,d] += (out[m,d] + bias[d]) * gate[d]`; eliminates one full [S,D] fp16 read-modify-write per call (~720 slots / denoise) | +| `fp16_add_bias_vec8` | `flash_rt_minimax_remover` | scalar `add_bias_fp16` (decoder_fused) | vectorised fp16x8 (uint4) in-place bias add; used for Q/K/V and any bias-only slot; ~8× fewer memory transactions | +| `fp16_ada_layernorm_quant_fp8` | `flash_rt_minimax_remover` | Triton `ada_layernorm_fp16_io` + `quantize_fp8_static_fp16` + 3× per-Linear activation quant (Q/K/V) | fused single-pass fp32-stat LayerNorm + adaLN modulation + per-tensor FP8 quantise; feeds a shared-scale FP8 tensor into Q/K/V (one quantise for three Linears, three descales) | +| `fp16_rmsnorm_rope_bshd` | `flash_rt_minimax_remover` | Triton `rms_norm_fp32stat` + `rope_apply_bshd` (2 kernels, 2 full R/W of Q or K) | fused per-token RMSNorm (fp32 stats + fp16 affine) + interleaved RoPE on native [B,S,H,Dd] fp16 layout; one full R/W per tensor; skips the intermediate fp16 write of the normalised Q/K | +| channels-last pipeline | Python (`_vae_opt.py`) | Conv3d weight → CL, WanCausalConv3d → CL-preserving forward | eliminates ~97% of nchw↔nhwc conversion kernels (~280 ms / decode) | +| Running-max amax | Python (`_vae_opt.py`) | separate `amax_fp16` calls over cache + new each iteration | norm fuses amax via atomicMax into a persistent buffer shared with the sister conv; cache amax is skipped entirely (covered by running max) | +| `WanUpsample` patch | Python (no kernel) | `x.float().type_as(x)` for nearest-exact upsample | eliminates redundant fp32 cast (index-only op, fp16 == fp32) | +| `nvfp4_conv3d_ndhwc_fp16out` | `flash_rt_minimax_remover` | FP8 conv3d for eligible 3×3×3 causal convs (Ci % 64 == 0) | **Purpose-built NVFP4 W4A4** implicit-GEMM conv3d using `mma.sync.kind::mxf4nvf4` (e2m1×e2m1, UE4M3 block scales). fp16 NDHWC output (eliminates bf16→fp16 + NCDHW→NDHWC conversions). 2× MMA throughput vs FP8. ~16% VAE speedup. | +| `fp16_quant_nvfp4_ndhwc` | `flash_rt_minimax_remover` | PyTorch multi-pass fp16→FP8 quant for VAE activations | Fused fp16 NCDHW → NVFP4 packed + UE4M3 block-scale (NDHWC output). Single-pass quantization with per-16-element block scales. | +| `fp16_quant_nvfp4_cl_ndhwc` | `flash_rt_minimax_remover` | same, for channels-last 3D input | Channels-last variant: reads NDHWC physical layout directly (channel is innermost → coalesced). Eliminates `contiguous()` copy. | +| `fp16_rms_silu_quant_nvfp4_cl_ndhwc` | `flash_rt_minimax_remover` | separate RMS+SiLU + fp16→FP4 quant (3 kernels) | Fused RMS_norm + SiLU + NVFP4 quantization in one kernel (channels-last input). fp32 statistics, fp32 SiLU (preserves fp16 mantissa precision). | +| Rolling 2-frame FP4 cache | Python (`_vae_nvfp4.py`) | per-call cache fp16→FP4 quantization | Stores quantized FP4+SF from the previous call; rolling 2-frame window handles T_new=1 (decode) by combining `[prev_frame, current_frame]`. Eliminates cache quantization per call. | + +The VAE stays fp16 at the interface. Norm/activation ops use fp16-native +kernels (zero precision loss). Applicable 3×3×3 causal conv3d layers +(Ci % 32 == 0) use the FP8 implicit-GEMM kernel — fp16 activations are +quantized to FP8 e4m3 on-the-fly, the MMA runs on tensor cores in FP8, +and the result is dequantized back to fp16 via per-output-channel scales +(PSNR ~39 dB vs fp16 reference). Non-applicable convs (1×1×1, 3×1×1, +Ci not divisible by 32) fall back to cuDNN fp16. If the module is not +built, `install_vae_optimizations()` raises a clear `ImportError` with +the rebuild command. + +#### Channels-last 3D pipeline + +cuDNN's fp16 conv3d kernel (`sm80_xmma_fprop_implicit_gemm`) internally +operates in NHWC. When the VAE feeds NCDHW tensors, cuDNN inserts +`nchwToNhwcKernel` / `nhwcToNchwKernel` conversion kernels before and +after **every** conv3d call — totalling ~287 ms for a 18-frame decode +(~11% of wall time). + +The `install_vae_optimizations()` call now enables a **channels-last +3D (NDHWC) pipeline** end-to-end: + +1. All 61 `WanCausalConv3d` weight tensors are converted to + `memory_format=torch.channels_last_3d` (done once at install time). +2. `WanCausalConv3d.forward` is patched to preserve the CL format + (cat + pad + conv3d all preserve CL natively). +3. The FlashRT norm kernels are swapped to NDHWC variants + (`fp16_rms_norm_ndhwc`, `fp16_rms_silu_ndhwc`) so the norm output + stays in CL — no format break between norm and conv. + +Result: **97% of format-conversion kernels eliminated** (287 ms → 9 ms), +plus a ~1.3× per-conv speedup from cuDNN's preferred CL algorithm. +Zero precision loss (PSNR 40.8 dB vs fp16 reference, identical to the +NCDHW path within fp16 rounding). + +#### FP8 implicit-GEMM conv3d pipeline + +The dominant VAE cost is cuDNN's fp16 conv3d (~1549 ms / decode, 64% of +wall time). The `fp8_conv3d_mm_ndhwc_fp16out` kernel replaces cuDNN for +applicable 3×3×3 causal conv3d layers (Ci % 32 == 0, Co >= 8) with a +**hand-rolled FP8 e4m3 implicit-GEMM** that runs entirely on tensor cores. + +Key design: + +- **No im2col materialization.** The kernel computes im2col indices + on-the-fly inside the MMA loop, reading activations directly from the + original NDHWC tensor via `cp.async`. This avoids the ~268 MB + intermediate matrix that made the naive FP8 im2col + `_scaled_mm` + approach 3.5× slower than cuDNN. +- **Virtual cache concat.** Two input pointers (`cache_x_fp8` + + `new_x_fp8`) replace `torch.cat`, saving the concat kernel. The + temporal addressing `d_in = t_out + kt` reads from cache for + `d_in < T_cache`, else from new — zero-copy causal sliding window. +- **Direct causal output.** Output is `T_new` frames (not + `T_cache + T_new`), avoiding the slice + wasted output write. +- **Per-output-channel weight quantization.** Weights are quantized + once at install time with per-Co amax scales, maximizing FP8 dynamic + range utilization. The dequant alpha = `act_scale × w_scale[co]` is + applied in the bias-fused epilogue. +- **Fused activation quantization.** `amax_fp16` + atomicMax computes + a shared per-tensor scale over cache+new (2-pass, no host sync), then + `quantize_fp16_fp8_with_amax` scales and casts to FP8 e4m3. +- **Running-max amax.** The norm module fuses amax into its + norm+silu pass via `fp16_rms_silu_amax_ndhwc` and accumulates it + into a persistent device-side buffer (atomicMax). Because cache_x + was a previous output of the same norm, its amax is already + covered — so the conv **skips the cache amax pass entirely** + (saves one full read of the 2-frame cache tensor per layer). +- **Dual-quantize launch.** `quantize_fp16_fp8_with_amax_dual` + quantizes the cache and new tensors in a single kernel launch, + reducing launch overhead by ~49 calls per decode. +- **Tile geometry.** BLOCK_M=128, BLOCK_N=128, BLOCK_K=32, 8 warps, + 2-stage cp.async pipeline, persistent Y-major CTA raster. The MMA + instruction is + `mma.sync.aligned.kind::f8f6f4.m16n8k32.row.col.f32.e4m3.e4m3.f32`. + +Result: **1.7% additional decode speedup** over the previous FP8 +conv3d path (8.75 s → 8.58 s) and **0.7 dB PSNR improvement** +(39.3 → 39.9 dB median) thanks to the temporally consistent +running-max scaling. Combined with the channels-last pipeline: +**2.02× vs baseline**. PSNR 39.9 dB (median). + +#### Fused transformer FFN epilogue + +The transformer denoise loop spends ~877 ms / 12-step run on three +elementwise "glue" kernels sandwiched between the FP8 GEMMs of each +FeedForward block: + + `add_bias_fp16` (607 ms) + `gelu_inplace_fp16` (233 ms) + `quantize_fp8` (270 ms) + +Each is a full read+write of the `[S, inner_dim]` fp16 FFN-up output +(inner_dim = 13824). The `bias_gelu_quant_fp16_fp8` kernel collapses +all three into a **single pass** that reads the GEMM's raw fp16 output +once, applies bias + tanh-GELU in fp32, and writes fp8 e4m3 directly. +The fp8 tensor becomes the pre-quantised input of the FFN-down Linear, +which skips its own activation quantise step. + +All arithmetic (bias + GELU) is done in fp32 before the fp8 cast, so +the result is actually **more accurate** than the original path (which +rounds to fp16 twice along the way) — end-to-end PSNR improves from +39.9 → 40.0 dB. + +The FP8 pipeline freezes calibration after the **first denoise step** +(via a one-shot transformer forward hook), so steps 2..N run with +static scales and the fused epilogue active — a single-call invocation +benefits without needing a separate warm-up pass. + +Result: **denoise GPU time 3.73 s → 3.16 s** (-15%), end-to-end +**8.58 s → 7.56 s** (**2.29× vs baseline**), PSNR **40.0 dB** (median). + +#### Fused O-proj / FFN-down bias + gate + residual + +Each transformer block finishes both the attention O-projection and +the FFN-down projection with the same 3-op tail: + + `add_bias_fp16` + `gate_mul_residual_bcast` → `residual += (out + bias) * gate[D]` + +That's 2 kernel launches + one full-tensor fp16 read-modify-write on +`out` per slot, and there are 30 blocks × 2 slots × 12 steps = **720 +occurrences per denoise**. `fp16_bias_gate_residual_bcast` collapses +the pair into a single fp16x8 (uint4) kernel that reads `out` once, +folds in the broadcast bias & gate, and writes straight into the +residual — eliminating the intermediate RMW pass. The Q/K projection +bias is fused directly into `fp16_rmsnorm_rope_quant_int8_q/k` (added +pre-norm in fp32, see #3 above) so the Q/K `add_bias` kernel is gone +entirely; `fp16_add_bias_vec8` now only handles V and proj_out. + +Result: **denoise GPU kernel time 3.10 s → 3.03 s** (-70 ms), end-to-end +**7.56 s → 7.57 s** (wall time is CPU/launch-bound at 12 steps × 30 +blocks × 432×240 — further wins require kernel-count reduction, e.g. +graph capture, rather than per-kernel savings). PSNR **40.0 dB** (median), +worst-frame **36.2 dB**. Overall stack now runs at **2.30× vs the fp16 +reference** (17.42 s → 7.57 s). + +An env-var toggle `FLASHRT_DISABLE_BIAS_GATE=1` disables this fusion +for A/B verification without a rebuild. + Importing `flash_rt.models.minimax_remover` always succeeds — it needs **none** of `diffusers` / `einops` / `scipy` / `triton` / `sageattention`. The kernel surface is validated lazily in each pipeline's `__init__` via @@ -85,11 +277,12 @@ kernels are self-contained Triton JIT kernels shipped in the package `flash_rt/models/minimax_remover/_fp8_pipeline.py`. Uses static calibration: the first inference call runs in dynamic-FP8 calibration mode (accumulating -activation amax on GPU), then freezes to a static `act_scale` for all -subsequent calls (zero CPU sync overhead in the steady state, suitable for -CUDA Graph capture). **The frozen scale is calibrated to the first call's -input; if the input resolution/shape changes, construct a new pipeline so -the scale is re-calibrated.** +activation amax on GPU). A one-shot transformer forward hook **freezes the +calibration after the first denoise step**, so steps 2..N (and the fused FFN +epilogue kernel) run with static scales — a single-call invocation benefits +without needing a separate warm-up pass. **The frozen scale is calibrated to +the first call's input; if the input resolution/shape changes, construct a +new pipeline so the scale is re-calibrated.** - every eligible transformer Linear -> FP8 W8A8 GEMM (weight quantised once at load time; activation quantised with a calibrated static scale); @@ -120,10 +313,17 @@ MiniMax-Remover `pipe` and consumes it in place: inside the captured graph there are **zero** torch elementwise ops — every operation is a kernel launch. -Both paths run the VAE encode / decode unchanged from the loaded diffusers -model (one-shot per segment, outside the graph). No MiniMax-Remover source -is imported; the `pipe` is duck-typed through `.transformer` / `.vae` / -`.scheduler` / `.video_processor` and the `expand_masks` / `resize` helpers. +Both paths run the VAE encode / decode from the loaded diffusers model +(one-shot per segment, outside the graph). With `--vae-opt` (default), the +VAE's `WanRMS_norm` / `WanResidualBlock` norm+silu sites are replaced by the +FlashRT fp16 fused kernels, all `WanCausalConv3d` weights are converted +to channels-last 3D, the norm kernels are swapped to NDHWC variants so +the entire norm→conv pipeline stays in channels-last, and applicable 3×3×3 +causal conv3d layers are replaced by the FP8 implicit-GEMM kernel +(see [VAE fused kernels](#vae-fused-kernels-standalone-module-opt-in)). +No MiniMax-Remover source is imported; the `pipe` is duck-typed through +`.transformer` / `.vae` / `.scheduler` / `.video_processor` and the +`expand_masks` / `resize` helpers. ## Performance (RTX 5060 Ti, SM120, CUDA 13) @@ -134,9 +334,10 @@ python3 examples/minimax_remover_quickstart.py \ --model-dir ./minimax-remover \ --frames-dir ./object_removal_data/ \ --masks-dir ./object_removal_data/ \ - --output-dir ./out # FP8 (default) -python3 examples/minimax_remover_quickstart.py ... --use-fp4 # NVFP4 -python3 examples/minimax_remover_quickstart.py ... --no-flashrt # fp16 reference + --output-dir ./out # FP8 + VAE opt (default) +python3 examples/minimax_remover_quickstart.py ... --no-vae-opt # FP8, no VAE kernels +python3 examples/minimax_remover_quickstart.py ... --use-fp4 # NVFP4 +python3 examples/minimax_remover_quickstart.py ... --no-flashrt # fp16 reference ``` Wall time is a single end-to-end segment (load -> encode -> denoise loop -> @@ -151,23 +352,103 @@ All rows compare against the non-FlashRT `--no-flashrt` fp16 reference on the | Clip (frames, resolution) | Stack | Wall time | Speedup vs fp16 ref | PSNR mean / worst vs fp16 ref | cosine mean | |--------------------------|-------|-----------|---------------------|-------------------------------|-------------| -| tennis (70 frames, 432x240) | fp16 reference (`--no-flashrt`) | 17.31 s | 1.0x | — | — | -| tennis (70 frames, 432x240) | FlashRT FP8 (default) | 11.76 s | **1.47x** | 40.8 / 37.4 dB | 0.99981 | -| tennis (70 frames, 432x240) | FlashRT NVFP4 (`--use-fp4`) | 9.52 s | 1.82x | 7.0 / 6.2 dB | 0.00000 (broken) | +| tennis (70 frames, 432x240) | fp16 reference (`--no-flashrt --no-vae-opt`) | 17.33 s | 1.0x | — | — | +| tennis (70 frames, 432x240) | FlashRT FP8 + VAE opt + CL (`--no-fp8-conv`) | 10.01 s | 1.73x | 40.8 / 37.0 dB | 0.99981 | +| tennis (70 frames, 432x240) | FlashRT FP8 + VAE opt + CL + FP8 conv3d | 8.58 s | 2.02x | 39.9 / 36.4 dB | 0.99981 | +| tennis (70 frames, 432x240) | FlashRT FP8 + VAE opt + CL + FP8 conv3d + fused FFN epilogue | 7.56 s | 2.29x | 40.0 / 36.4 dB | 0.99981 | +| tennis (70 frames, 432x240) | FlashRT FP8 + VAE opt + CL + FP8 conv3d + fused FFN epilogue + fused bias-gate residual | 7.57 s | 2.30x | 40.0 / 36.2 dB | 0.99981 | +| tennis (70 frames, 432x240) | **… + fused adaLN+quant (shared-scale QKV) + fused RMSNorm+RoPE + fused norm2+quant (FP8-only stack, `--no-nvfp4-vae`)** | **7.18 s** | **2.41x** | **40.0 / 36.0 dB** | 0.99981 | +| tennis (70 frames, 432x240) | **… + NVFP4 VAE (38 conv layers → W4A4, FP4 cache)** | **6.71 s** | **2.58x** | **34.7 / 30.6 dB** | 0.99981 | +| tennis (70 frames, 432x240) | **… + NVFP4 fused norm+silu+quant + FP4 cache reuse (#1) + FP8 fused norm+silu+amax+quant (#2) + Q/K bias fused into rmsnorm+rope+quant (#3, current default)** | **6.56 s** | **2.64x** | **35.2 / 31.7 dB** | 0.99918 | +| tennis (70 frames, 432x240) | FlashRT NVFP4 transformer (`--use-fp4`) | 9.52 s | 1.82x | 7.0 / 6.2 dB | 0.00000 (broken — transformer FP4 error accumulates over 12 denoise steps) | | bmx-trees (80 frames, 432x240) | fp16 reference (`--no-flashrt`) | 19.76 s | 1.0x | — | — | | bmx-trees (80 frames, 432x240) | FlashRT FP8 (default) | 13.24 s | **1.49x** | 35.1 / 32.0 dB | 0.99912 | -| bmx-trees (80 frames, 432x240) | FlashRT NVFP4 (`--use-fp4`) | 10.72 s | 1.84x | 7.3 / 7.0 dB | 0.00000 (broken) | +| bmx-trees (80 frames, 432x240) | FlashRT NVFP4 transformer (`--use-fp4`) | 10.72 s | 1.84x | 7.3 / 7.0 dB | 0.00000 (broken — transformer FP4 error accumulates over 12 denoise steps) | + +> Tennis numbers are from a same-session serial A/B (`--no-flashrt +> --no-vae-opt` vs default FlashRT FP8 stack) measured on RTX 5060 Ti, +> CUDA 13.0, cuDNN 9.2, PyTorch 2.12. Run one process at a time — +> parallel invocations contend on the GPU and inflate wall time. +> Peak VRAM: fp16 ref 3.67 GB, FlashRT default 2.57 GB. +> Steady-state (2nd call, post-calibration): default FlashRT ~5.73 s +> (~3.0× vs fp16 ref); single-call numbers above include the one-shot +> FP8 calibration on the first call. Takeaways: -- **FP8 is the correct default for full-frame inpainting**: ~1.5x faster than - the fp16 reference with cosine >= 0.999 and PSNR 35-41 dB. -- **NVFP4 is faster but unusable on full-frame latents**: cosine collapses to +- **The current default** (all of the below + NVFP4 VAE + #1/#2/#3 fused + norm+quant/bias) is **2.64× faster** than the fp16 reference (17.34 → + 6.56 s) with PSNR **35.2 dB** mean over 69 inpainted frames, peak VRAM + 2.57 GB (vs 3.67 GB fp16 ref, –30%). Add `--no-nvfp4-vae` and set + `FLASHRT_NVFP4_FUSED_NORMQUANT=0 FLASHRT_FP8_FUSED_NORMQUANT=0` for the + higher-precision FP8-only stack (~40 dB, ~7.2 s) when absolute fidelity + matters more than speed. The fused FFN epilogue kernel + (`bias_gelu_quant_fp16_fp8`) collapses bias-add + GELU + activation + quantise into one pass, cutting denoise GPU time by 15% (3.73 → 3.16 s) + and end-to-end from 8.58 → 7.56 s. The fused bias + gate + residual + kernel (`fp16_bias_gate_residual_bcast`) further collapses the O-proj + and FFN-down block tails (720 slots / denoise) into single-pass + fp16x8 kernels, trimming denoise GPU kernel time another –70 ms + (wall 7.56 → 7.57 s, launch-bound). The fused adaLN+quant kernel + (`fp16_ada_layernorm_quant_fp8`) collapses the per-block LayerNorm + + adaLN modulation + 3× per-Linear activation quant into a single pass + and feeds a **shared-scale FP8 tensor** into Q/K/V (one quantise, + three descales), boosting PSNR to 40.8 dB because the shared max + scale suppresses per-Linear outlier saturation. The fused + `fp16_rmsnorm_rope_bshd` kernel then collapses the Q/K RMSNorm + + interleaved RoPE into a single per-token pass, eliminating one full + fp16 R/W of each Q/K tensor per attention block. Combined wall time + drops 7.57 → 7.28 s (–4%). Latest serial measurement (same-session A/B, + one process at a time, no GPU contention) confirms **7.18 s / 2.41x**. +- **FP8 conv3d vs channels-last-only cuDNN**: the hand-rolled implicit-GEMM + kernel (no im2col materialization, virtual cache concat, per-channel + weight dequant, fused amax, running-max scale) beats cuDNN's fp16 conv3d + while staying in fp16 at the interface. Disable with `--no-fp8-conv` to + recover ~1 dB PSNR if absolute precision is preferred over speed. +- **NVFP4 VAE (default ON)**: the purpose-built `nvfp4_conv3d_ndhwc_fp16out` + kernel replaces 38 eligible 3×3×3 conv3d layers (Ci ≥ 192) in the WanVAE + with NVFP4 W4A4 MMA (`mma.sync.kind::mxf4nvf4`, 2× tensor-core throughput + vs FP8). Key optimizations: (1) fp16 NDHWC output — eliminates bf16→fp16 + + NCDHW→NDHWC conversions; (2) channels-last direct input — eliminates + `contiguous()` copy; (3) rolling 2-frame FP4 cache — eliminates per-call + cache re-quantization. VAE encode 14.5% faster, decode 13.6% faster + (VAE total 3493→3002 ms, –16.4%). End-to-end 7.18→6.71 s (**2.58× vs fp16 + reference**). PSNR 34.7 dB (median) vs fp16 — «近乎一致(优秀)». Disable + with `--no-nvfp4-vae`. +- **Fused norm+quant + bias-into-rmsnorm (#1/#2/#3, default ON)**: three + additional fusion points, all quality-neutral (end-to-end PSNR unchanged + or slightly higher), contributing ~125 ms (~2.1%) steady-state: + - **#1 NVFP4 fused norm+silu+NVFP4-quant + FP4 cache reuse** + (`fp16_rms_silu_quant_nvfp4_cl_ndhwc`, env `FLASHRT_NVFP4_FUSED_NORMQUANT=1`): + the sister norm of each fully-NVFP4 `WanResidualBlock` produces the FP4 + activation directly in one kernel (no fp16 round-trip), and the conv + reuses Direction-2's rolling 2-frame FP4 cache. **Critical**: the WanVAE + streams temporally (`_decode` loops one frame per call), so the cache + must mirror diffusers' `[prev, current]` padding — without it PSNR + collapses to ~27 dB. + - **#2 FP8 fused norm+silu+running-amax+FP8-quant** + (`fp16_rms_silu_amax_quant_fp8_ndhwc_nozero`, env + `FLASHRT_FP8_FUSED_NORMQUANT=1`): same idea for the FP8-conv residual + blocks. Uses the `_nozero` variant (accumulates into the running amax + instead of zeroing) so the new-x FP4 scale stays consistent with the + causal cache's running scale. + - **#3 Q/K bias fused into rmsnorm+RoPE+int8-quant**: cuBLASLt's + `CUBLASLT_EPILOGUE_BIAS` is NOT_SUPPORTED for FP8 (e4m3) GEMMs, so the + Q/K bias is fused into the *downstream* `fp16_rmsnorm_rope_quant_int8` + kernel (added pre-norm in fp32). Eliminates 720 of 1092 `add_bias_vec8` + calls per denoise (Q+K of every block); V and proj_out keep their bias + (V is not normed downstream). `gemm_from_fp8_ext_nobias` feeds the + no-bias Q/K GEMM output. + Combined steady-state 5.86 → 5.73 s; PSNR 35.16 dB mean / 31.73 worst + (vs 35.11 at the pre-fusion baseline) — the fp32 bias add in #3 is + slightly *more* precise than the fp16 bias-add it replaces. +- **NVFP4 transformer (`--use-fp4`) is unusable on full-frame latents**: cosine collapses to ~0.0 and PSNR to ~7 dB (median per-pixel deviation ~85/255). The FP4 - quantisation error accumulates over the large full-frame activations and the - output drifts to black — exactly why FP8 is the default. NVFP4 is only - appropriate for small cropped regions, where its per-block error stays - bounded. + quantisation error accumulates over the 12-step denoise loop in the transformer + and the output drifts to black. NVFP4 transformer is only appropriate for + small cropped regions, where its per-block error stays bounded. The NVFP4 + **VAE** path (above) is unaffected because the VAE is a single-pass + encoder/decoder with no iterative error accumulation. ### Transformer GEMM (NVFP4 vs fp16 matmul, single layer) @@ -190,13 +471,25 @@ to the quantised GEMMs and the attention backend. | Component | Metric | Value | |-----------|--------|-------| +| VAE `fp16_rms_norm_ncdhw` kernel | cosine vs fp32 reference | >= 0.9999999 | +| VAE `fp16_rms_silu_ncdhw` kernel | cosine vs fp32 reference | >= 0.9999999 | +| VAE `fp16_rms_norm_ndhwc` (CL) kernel | cosine vs fp32 reference | >= 0.9999999 | +| VAE `fp16_rms_silu_ndhwc` (CL) kernel | cosine vs fp32 reference | >= 0.9999999 | +| VAE `fp16_rms_silu_amax_ndhwc` kernel | amax vs fp32 reference | exact (atomicMax on non-negative floats) | +| VAE `fp8_conv3d_mm` kernel | cosine vs fp16 F.conv3d | >= 0.9993 (per-layer) | +| End-to-end FP8 + VAE opt + CL + FP8 conv3d + fused FFN epilogue (full-frame) | PSNR vs fp16 ref | 40.0 dB (median) / >= 36.0 dB (worst frame) | +| End-to-end FP8 + VAE opt + CL + FP8 conv3d (full-frame) | PSNR vs fp16 ref | 39.9 dB (median) / >= 36.4 dB (worst frame) | +| End-to-end FP8 + VAE opt + CL only (no FP8 conv3d) | PSNR vs fp16 ref | 40.8 dB (median) / >= 37.0 dB (worst frame) | | Attention — SageAttention QK-int8 PV-fp8 (`sage_fp8`, default) | cosine vs SDPA | 0.9993 | | Attention — SageAttention QK-int8 PV-fp16 (`sage_fp16`) | cosine vs SDPA | 0.9999 | | NVFP4 W4A4 GEMM | cosine vs fp16 matmul | >= 0.999 | | FP8 W8A8 GEMM | cosine vs fp16 matmul | >= 0.999 | -| End-to-end FP8 (full-frame) | PSNR vs fp16 ref | 35-41 dB (mean) / >= 32 dB (worst frame) | +| End-to-end FP8 (full-frame) | PSNR vs fp16 ref | 35-41 dB (mean 39.9) / >= 36 dB (worst frame) | +| End-to-end FP8 + NVFP4 VAE (full-frame) | PSNR vs fp16 ref | 35.2 dB (mean) / >= 31.7 dB (worst frame) | +| End-to-end FP8 + NVFP4 VAE (full-frame) | PSNR vs FP8 baseline | 35.8 dB (mean) — same math, fp4/fp8 quant noise only | +| End-to-end FP8 + NVFP4 VAE (full-frame) | cosine vs fp16 ref | >= 0.99918 (mean 0.99918) | | End-to-end FP8 (full-frame) | cosine vs fp16 ref | >= 0.999 | -| End-to-end NVFP4 (full-frame) | cosine vs fp16 ref | ~0.0 — **broken**, output drifts to black (median per-pixel deviation ~85 / 255) | +| End-to-end NVFP4 transformer (full-frame) | cosine vs fp16 ref | ~0.0 — **broken**, output drifts to black (FP4 error accumulates over 12 denoise steps in the transformer) | | End-to-end NVFP4 (small cropped region only) | PSNR vs fp16 ref | ~52 dB (mean) / ~45 dB (worst frame); per-block FP4 error stays bounded only when activations are small | The default `sage_fp8` attention gives the best latency at cosine 0.9993; @@ -204,6 +497,16 @@ switch to `FLASHRT_ATTN_MODE=sage_fp16` for cosine 0.9999 at a small latency cost. NVFP4 needs no calibration, so the first call is already in the steady state; the FP8 path calibrates on the first call then freezes. +## CLI flags (quickstart) + +| Flag | Default | Effect | +|------|---------|--------| +| `--vae-opt` / `--no-vae-opt` | **enabled** | Install FlashRT fp16 fused VAE kernels (`fp16_rms_norm_ncdhw`, `fp16_rms_silu_ncdhw`, NDHWC variants, fused norm+silu+amax) + channels-last 3D pipeline + running-max amax sharing between norm and conv + FP8 implicit-GEMM conv3d + `WanUpsample` cast elimination. Requires the `flash_rt_minimax_remover` module. | +| `--fp8-conv` / `--no-fp8-conv` | **enabled** | Use FP8 implicit-GEMM conv3d kernel for applicable 3×3×3 causal convs (requires `--vae-opt`). Trades ~1 dB PSNR for ~14% decode speedup over channels-last-only cuDNN. | +| `--use-fp4` | off | Use NVFP4 (W4A4) transformer instead of FP8 (W8A8). Small-region only — broken on full-frame. | +| `--no-nvfp4-vae` | off | Disable NVFP4 W4A4 VAE conv3d (default: enabled). Uses purpose-built NVFP4 MMA kernel for eligible VAE conv layers (Ci>=192) for ~16% VAE speedup. | +| `--no-flashrt` | off | Run the reference diffusers fp16 path (no FlashRT). | + ## Environment variables | Variable | Default | Effect | @@ -216,6 +519,14 @@ the steady state; the FP8 path calibrates on the first call then freezes. | `FLASHRT_FP8_TARGET` | `all` | FP8 Linear scope (`all` / `ffn_only`) (FP8 path) | | `FLASHRT_NORM_MODE` | `triton` | per-block LayerNorm kernel: `triton` (fp32-stat Triton, bit-exact) / `fp16` (FlashRT `ada_layer_norm_fp16`, lower precision, debug only) | | `FLASHRT_GELU_MODE` | `inplace` | FFN GELU kernel: `inplace` (FlashRT fused `gelu_inplace*`) / `torch` (original `F.gelu`, debug only) | +| `FLASHRT_NVFP4_VAE` | `1` | Enable NVFP4 VAE conv3d (default ON). Set to `0` to disable. | +| `FLASHRT_NVFP4_VAE_DECODE_ONLY` | `0` | `1` = apply NVFP4 only to decoder (encoder stays FP8); `0` = both encode+decode. | +| `FLASHRT_NVFP4_VAE_MIN_CI` | `192` | Minimum Ci for NVFP4 eligibility. 192 covers WanVAE Ci=192/384; 384 is stricter (better PSNR, fewer layers). | +| `FLASHRT_NVFP4_NO_CACHE` | `0` | `1` = disable FP4 cache (re-quantize cache per call); `0` = use rolling 2-frame FP4 cache (default, faster). | +| `FLASHRT_NVFP4_FUSED_NORMQUANT` | `1` | #1: `1` (default) = patch `WanResidualBlock.forward` for fully-NVFP4 blocks so the sister norm emits FP4 directly (fused norm+silu+NVFP4-quant) and the conv reuses the rolling FP4 cache. `0` = separate norm→fp16→quant path (Direction-2). | +| `FLASHRT_FP8_FUSED_NORMQUANT` | `1` | #2: `1` (default) = same fused norm+silu+running-amax+FP8-quant for the FP8-conv residual blocks (`fp16_rms_silu_amax_quant_fp8_ndhwc_nozero`). `0` = separate norm+amax then quantize_dual. | +| `FLASHRT_FP8_EAGER_MANUAL` | `1` | `1` (default) = steady-state denoise runs the eager manual loop (avoids per-step `torch.cat` + scheduler CPU sync). `0` = diffusers `__call__`. | +| `FLASHRT_FP8_GRAPH` | `0` | `1` = capture the whole denoise loop as one CUDA Graph (FP8 path; loses ~1.1 s due to forcing a slower graph-safe attention backend — not recommended). | ## Usage @@ -238,7 +549,38 @@ output = pipeline( video = output.frames ``` -### NVFP4 (small cropped regions only) +### FP8 + NVFP4 VAE (recommended default — full-frame, fastest) + +The NVFP4 VAE optimization is **enabled by default** when using the FP8 +transformer path with VAE optimizations. No extra code needed — just +construct the FP8 pipeline and call `install_vae_optimizations` + +`install_vae_nvfp4`: + +```python +from flash_rt.models.minimax_remover import MiniMaxRemoverPipelineFP8 +from flash_rt.models.minimax_remover._vae_opt import install_vae_optimizations +from flash_rt.models.minimax_remover._vae_nvfp4 import install_vae_nvfp4 + +pipeline = MiniMaxRemoverPipelineFP8(pipe) +install_vae_optimizations(pipe.vae, use_fp8_conv=True) +install_vae_nvfp4(pipe.vae) # 38 conv layers → NVFP4 W4A4 (default ON) + +output = pipeline( + images=frames, masks=masks, num_frames=len(frames), + height=720, width=1280, num_inference_steps=12, +) +``` + +Or via the quickstart (NVFP4 VAE is on by default): + +```bash +python3 examples/minimax_remover_quickstart.py \ + --model-dir ./minimax-remover \ + --frames-dir ./frames --masks-dir ./masks --output-dir ./out +# Add --no-nvfp4-vae to disable +``` + +### NVFP4 transformer (small cropped regions only) ```python from flash_rt.models.minimax_remover import MiniMaxRemoverPipeline diff --git a/examples/minimax_remover_quickstart.py b/examples/minimax_remover_quickstart.py index a4cbcb72..14487320 100644 --- a/examples/minimax_remover_quickstart.py +++ b/examples/minimax_remover_quickstart.py @@ -38,10 +38,12 @@ Build ------------------------------------------------------------------ FlashRT must be built for Blackwell so the generic SM120 NVFP4 kernels -are compiled in (GPU_ARCH=120 / 121 auto-enables them): +are compiled in (GPU_ARCH=120 / 121 auto-enables them). The VAE +fused fp16 kernels are opt-in (FLASHRT_ENABLE_MINIMAX_REMOVER=ON): - cmake -S . -B build -DGPU_ARCH=120 -DCMAKE_BUILD_TYPE=Release - cmake --build build -j --target flash_rt_kernels + cmake -S . -B build -DGPU_ARCH=120 -DCMAKE_BUILD_TYPE=Release \ + -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON + cmake --build build -j --target flash_rt_kernels flash_rt_minimax_remover pip install -e ".[torch,minimax-remover]" ------------------------------------------------------------------ @@ -51,7 +53,8 @@ --model-dir ./minimax-remover \ --frames-dir ./object_removal_data/ \ --masks-dir ./object_removal_data/ \ - --output-dir ./out + --output-dir ./out # FP8 + VAE opt (default) + python3 examples/minimax_remover_quickstart.py ... --no-vae-opt # FP8 only ------------------------------------------------------------------ Precision note @@ -589,6 +592,23 @@ def parse_args() -> argparse.Namespace: "black/drift outputs. FP8 (default) is recommended " "for full-frame inpainting (end-to-end cosine >= 0.999, " "PSNR ~35-41 dB vs fp16).") + p.add_argument("--vae-opt", action=argparse.BooleanOptionalAction, + default=True, + help="Apply FlashRT VAE optimisations: fused fp16 RMS_norm " + "+ RMS_SiLU CUDA kernels and WanUpsample cast " + "elimination. The VAE is ~60%% of wall time on the " + "FP8 path; this cuts it significantly. PSNR >= 39 dB " + "vs the fp16 VAE reference. (default: enabled; use " + "--no-vae-opt to disable)") + p.add_argument("--fp8-conv", action=argparse.BooleanOptionalAction, + default=True, + help="Use FP8 implicit-GEMM conv3d kernel for applicable " + "3x3x3 causal convs (requires --vae-opt). Trades ~1.5 " + "dB PSNR for ~13%% decode speedup. (default: enabled)") + p.add_argument("--no-nvfp4-vae", action="store_true", + help="Disable NVFP4 W4A4 VAE conv3d (default: enabled). " + "Uses purpose-built NVFP4 MMA kernel for eligible " + "decode conv layers (Ci>=192) for ~3-7%% VAE speedup.") return p.parse_args() @@ -643,6 +663,19 @@ def main() -> None: pipe = build_pipeline(model_dir) + if args.vae_opt: + from flash_rt.models.minimax_remover._vae_opt import install_vae_optimizations + stats = install_vae_optimizations(pipe.vae, + use_fp8_conv=args.fp8_conv) + print(f" VAE optimised: {stats}") + + # NVFP4 VAE conv3d (default ON for FlashRT FP8 path; --no-nvfp4-vae to disable) + if args.vae_opt and not args.no_flashrt and not args.use_fp4 and not args.no_nvfp4_vae: + from flash_rt.models.minimax_remover._vae_nvfp4 import install_vae_nvfp4 + nvfp4_stats = install_vae_nvfp4(pipe.vae) + if nvfp4_stats.get('enabled'): + print(f" NVFP4 VAE: {nvfp4_stats.get('n_quantized', 0)} layers → W4A4") + if args.no_flashrt: runner = pipe tag = "reference (diffusers fp16)" diff --git a/flash_rt/models/minimax_remover/_attention.py b/flash_rt/models/minimax_remover/_attention.py index 9718412a..88f34dcd 100644 --- a/flash_rt/models/minimax_remover/_attention.py +++ b/flash_rt/models/minimax_remover/_attention.py @@ -34,6 +34,50 @@ from ._kernels import rms_norm_fp32stat, rope_apply_bshd, freqs_to_cos_sin +# Optional MiniMax-Remover fused kernels — used when both are available. +_fvk = None +try: + from flash_rt import flash_rt_minimax_remover as _fvk +except ImportError: + try: + import flash_rt_minimax_remover as _fvk # type: ignore + except ImportError: + pass +_has_fused_rmsnorm_rope = ( + _fvk is not None and hasattr(_fvk, "fp16_rmsnorm_rope_bshd") + and os.environ.get("FLASHRT_DISABLE_RMSNORM_ROPE", "0") != "1") + +_has_fused_rmsnorm_rope_quant = ( + _fvk is not None + and hasattr(_fvk, "fp16_rmsnorm_rope_quant_int8_q") + and hasattr(_fvk, "fp16_rmsnorm_rope_quant_int8_k") + and os.environ.get("FLASHRT_DISABLE_FUSED_QUANT", "0") != "1") + +_SM89_COMPILE = None +_PER_CHANNEL_FP8 = None + + +def _get_sm89(): + global _SM89_COMPILE + if _SM89_COMPILE is None: + try: + from sageattention import sm89_compile + _SM89_COMPILE = sm89_compile + except ImportError: + _SM89_COMPILE = False + return _SM89_COMPILE if _SM89_COMPILE is not False else None + + +def _get_per_channel_fp8(): + global _PER_CHANNEL_FP8 + if _PER_CHANNEL_FP8 is None: + try: + from sageattention.quant import per_channel_fp8 + _PER_CHANNEL_FP8 = per_channel_fp8 + except ImportError: + _PER_CHANNEL_FP8 = False + return _PER_CHANNEL_FP8 if _PER_CHANNEL_FP8 is not False else None + try: _NUM_SMS = torch.cuda.get_device_properties(0).multi_processor_count except Exception: @@ -145,34 +189,174 @@ class FlashRTFA2Processor: def __init__(self): self._lse_bufs = {} self._cos_sin = {} + # Persistent [B*S] fp32 scratch for the fused rmsnorm+rope+int8-quant + # Q/K kernels (avoids a per-call cudaMallocAsync in the hot path). + self._rstd_bufs = {} def __call__(self, attn, hidden_states, rotary_emb=None, - attention_mask=None, encoder_hidden_states=None): + attention_mask=None, encoder_hidden_states=None, + no_out_bias=False, fp8_hidden=None, fp8_scale=None): mode = _attention_mode() B, S, _ = hidden_states.shape H = attn.heads Dd = attn.inner_dim // H scale = 1.0 / math.sqrt(float(Dd)) - q = attn.to_q(hidden_states) - k = attn.to_k(hidden_states) - v = attn.to_v(hidden_states) - if attn.norm_q is not None: - q = rms_norm_fp32stat(q, attn.norm_q.weight, attn.norm_q.eps) - if attn.norm_k is not None: - k = rms_norm_fp32stat(k, attn.norm_k.weight, attn.norm_k.eps) - - q = q.view(B, S, H, Dd) - k = k.view(B, S, H, Dd) - v = v.view(B, S, H, Dd) - + # ── Fused QKV entry from pre-quantised fp8 input ── + # When the caller provides the fp8 output of the fused adaLN+quant + # kernel plus the shared scale used to build it, drive Q/K/V from + # that fp8 tensor directly — saves 3 activation-quantise passes + # (one per Linear) and 1 fp16 read of norm1_out. Both Linears + # must expose gemm_from_fp8_ext (FlashRTFp8Linear post-calibration). + _use_fp8 = (fp8_hidden is not None and fp8_scale is not None + and hasattr(attn.to_q, "gemm_from_fp8_ext") + and hasattr(attn.to_k, "gemm_from_fp8_ext") + and hasattr(attn.to_v, "gemm_from_fp8_ext")) + cs = None if rotary_emb is not None: cs = self._cos_sin.get(S) if cs is None: cs = freqs_to_cos_sin(rotary_emb) self._cos_sin[S] = cs - rope_apply_bshd(q, cs[0], cs[1]) - rope_apply_bshd(k, cs[0], cs[1]) + + _fuse_qk = (_has_fused_rmsnorm_rope and cs is not None + and attn.norm_q is not None and attn.norm_k is not None + and (Dd & 7) == 0) + # ── Fully-fused path: RMSNorm + RoPE + int8 quant → sm89 attn ── + # Eliminates the fp16 intermediate between norm+rope and QK quantize. + _fuse_quant = (_fuse_qk and _has_fused_rmsnorm_rope_quant + and mode in ("sage_fp8", "sage2") + and _get_sm89() is not None + and _get_per_channel_fp8() is not None) + + # Q/K GEMM: when the Q/K bias will be fused into the downstream + # rmsnorm+rope+quant kernel (pre-norm add), fetch Q/K WITHOUT bias + # to avoid adding it twice. V always keeps its bias (not normed). + _qk_nobias = (_use_fp8 and _fuse_quant + and hasattr(attn.to_q, "gemm_from_fp8_ext_nobias")) + if _use_fp8: + if _qk_nobias: + q = attn.to_q.gemm_from_fp8_ext_nobias(fp8_hidden, fp8_scale) + k = attn.to_k.gemm_from_fp8_ext_nobias(fp8_hidden, fp8_scale) + else: + q = attn.to_q.gemm_from_fp8_ext(fp8_hidden, fp8_scale) + k = attn.to_k.gemm_from_fp8_ext(fp8_hidden, fp8_scale) + v = attn.to_v.gemm_from_fp8_ext(fp8_hidden, fp8_scale) + else: + q = attn.to_q(hidden_states) + k = attn.to_k(hidden_states) + v = attn.to_v(hidden_states) + if _fuse_quant: + if not q.is_contiguous(): + q = q.contiguous() + if not k.is_contiguous(): + k = k.contiguous() + stream = torch.cuda.current_stream().cuda_stream + D = H * Dd + + num_groups_q = (S + 31) // 32 + num_groups_k = (S + 63) // 64 + q_int8 = torch.empty(B * S, D, device=q.device, dtype=torch.int8) + k_int8 = torch.empty(B * S, D, device=q.device, dtype=torch.int8) + q_scale = torch.empty(B, H, num_groups_q, device=q.device, + dtype=torch.float32) + k_scale = torch.empty(B, H, num_groups_k, device=q.device, + dtype=torch.float32) + + # Q/K bias fused pre-norm when the nobias GEMM was used. + q_bias_ptr = (attn.to_q.bias.data_ptr() + if (_qk_nobias and attn.to_q.bias is not None) else 0) + k_bias_ptr = (attn.to_k.bias.data_ptr() + if (_qk_nobias and attn.to_k.bias is not None) else 0) + + # Reuse a persistent rstd scratch (B*S fp32) across Q/K calls so + # the fused kernel does zero hot-path allocation. Q and K run + # sequentially on the same stream, so one buffer serves both. + rstd = self._rstd_bufs.get((B, S)) + if rstd is None or rstd.device != q.device: + rstd = torch.empty(B * S, dtype=torch.float32, device=q.device) + self._rstd_bufs[(B, S)] = rstd + rstd_ptr = rstd.data_ptr() + + rc = _fvk.fp16_rmsnorm_rope_quant_int8_q( + q.data_ptr(), attn.norm_q.weight.data_ptr(), + q_bias_ptr, + cs[0].data_ptr(), cs[1].data_ptr(), + q_int8.data_ptr(), q_scale.data_ptr(), + B, S, H, Dd, float(attn.norm_q.eps), 1.0, + rstd_ptr, stream) + if rc != 0: + raise RuntimeError( + f"fp16_rmsnorm_rope_quant_int8_q failed rc={rc} " + f"(B={B} S={S} H={H} Dd={Dd})") + rc = _fvk.fp16_rmsnorm_rope_quant_int8_k( + k.data_ptr(), attn.norm_k.weight.data_ptr(), + k_bias_ptr, + cs[0].data_ptr(), cs[1].data_ptr(), + 0, # no smooth_k (negligible impact, saves k.mean compute) + k_int8.data_ptr(), k_scale.data_ptr(), + B, S, H, Dd, float(attn.norm_k.eps), 1.0, + rstd_ptr, stream) + if rc != 0: + raise RuntimeError( + f"fp16_rmsnorm_rope_quant_int8_k failed rc={rc} " + f"(B={B} S={S} H={H} Dd={Dd})") + + v = v.view(B, S, H, Dd) + if not v.is_contiguous(): + v = v.contiguous() + + per_channel_fp8 = _get_per_channel_fp8() + v_fp8, v_scale, _ = per_channel_fp8( + v, tensor_layout="NHD", scale_max=2.25, smooth_v=False) + + q_int8 = q_int8.view(B, S, H, Dd) + k_int8 = k_int8.view(B, S, H, Dd) + out = torch.empty(B, S, H, Dd, device=q.device, dtype=q.dtype) + + sm89 = _get_sm89() + sm89.qk_int8_sv_f8_accum_f16_fuse_v_scale_attn_inst_buf( + q_int8, k_int8, v_fp8, out, q_scale, k_scale, v_scale, + 0, 0, 2, scale, 0) + + hidden_states = out.view(B, S, H * Dd) + to_out0 = attn.to_out[0] + if no_out_bias and hasattr(to_out0, "gemm_no_bias"): + hidden_states = to_out0.gemm_no_bias(hidden_states) + else: + hidden_states = to_out0(hidden_states) + return hidden_states + + if _fuse_qk: + if not q.is_contiguous(): + q = q.contiguous() + if not k.is_contiguous(): + k = k.contiguous() + stream = torch.cuda.current_stream().cuda_stream + _fvk.fp16_rmsnorm_rope_bshd( + q.data_ptr(), attn.norm_q.weight.data_ptr(), + cs[0].data_ptr(), cs[1].data_ptr(), + B, S, H, Dd, float(attn.norm_q.eps), stream) + _fvk.fp16_rmsnorm_rope_bshd( + k.data_ptr(), attn.norm_k.weight.data_ptr(), + cs[0].data_ptr(), cs[1].data_ptr(), + B, S, H, Dd, float(attn.norm_k.eps), stream) + q = q.view(B, S, H, Dd) + k = k.view(B, S, H, Dd) + v = v.view(B, S, H, Dd) + else: + if attn.norm_q is not None: + q = rms_norm_fp32stat(q, attn.norm_q.weight, attn.norm_q.eps) + if attn.norm_k is not None: + k = rms_norm_fp32stat(k, attn.norm_k.weight, attn.norm_k.eps) + + q = q.view(B, S, H, Dd) + k = k.view(B, S, H, Dd) + v = v.view(B, S, H, Dd) + + if cs is not None: + rope_apply_bshd(q, cs[0], cs[1]) + rope_apply_bshd(k, cs[0], cs[1]) if not q.is_contiguous(): q = q.contiguous() @@ -185,7 +369,11 @@ def __call__(self, attn, hidden_states, rotary_emb=None, lse_cache=self._lse_bufs) hidden_states = out.view(B, S, H * Dd) - hidden_states = attn.to_out[0](hidden_states) + to_out0 = attn.to_out[0] + if no_out_bias and hasattr(to_out0, "gemm_no_bias"): + hidden_states = to_out0.gemm_no_bias(hidden_states) + else: + hidden_states = to_out0(hidden_states) return hidden_states diff --git a/flash_rt/models/minimax_remover/_fp8_linear.py b/flash_rt/models/minimax_remover/_fp8_linear.py index 713ebdf3..68039ed8 100644 --- a/flash_rt/models/minimax_remover/_fp8_linear.py +++ b/flash_rt/models/minimax_remover/_fp8_linear.py @@ -25,6 +25,20 @@ from flash_rt import flash_rt_kernels as kern +# Optional: FlashRT MiniMax-Remover extra kernel module. Used for the +# vectorised fp16 bias-add kernel (a drop-in replacement for the scalar +# `add_bias_fp16` in decoder_fused.cu) — cuts the Q/K/V bias cost. +_fvk_extra = None +try: + from flash_rt import flash_rt_minimax_remover as _fvk_extra +except ImportError: + try: + import flash_rt_minimax_remover as _fvk_extra # type: ignore + except ImportError: + _fvk_extra = None +_has_add_bias_vec8 = _fvk_extra is not None and hasattr( + _fvk_extra, "fp16_add_bias_vec8") + logger = logging.getLogger(__name__) _FP8 = torch.float8_e4m3fn @@ -158,8 +172,14 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: m, n, k, scale.data_ptr(), self.weight_scale.data_ptr(), stream, ) if self.bias is not None: - # FlashRT fused bias (in-place [m,n]+=bias[n]), no torch op - kern.add_bias_fp16(out.data_ptr(), self.bias.data_ptr(), m, n, stream) + # Prefer the vectorised fp16x8 bias-add kernel (8× fewer memory + # transactions than the scalar decoder_fused implementation). + if _has_add_bias_vec8 and (n & 7) == 0: + _fvk_extra.fp16_add_bias_vec8( + out.data_ptr(), self.bias.data_ptr(), m, n, stream) + else: + kern.add_bias_fp16(out.data_ptr(), self.bias.data_ptr(), + m, n, stream) out = out.view(*orig_shape[:-1], n) if in_dtype != torch.float16: @@ -175,6 +195,154 @@ def freeze_act_scale(self, margin: float = 1.0): self.act_scale.data = torch.tensor([scale], dtype=torch.float32, device=self.weight_fp8.device) self.calibrating = False + # ── Fused-FFN helpers (used by _kern_block block_forward) ── + # These let the FFN path skip 3 full-tensor round-trips between the + # proj0 (up) and proj1 (down) Linears by fusing bias+gelu+quant into + # one kernel (bias_gelu_quant_fp16_fp8 in flash_rt_minimax_remover). + + def gemm_no_bias(self, x: torch.Tensor) -> torch.Tensor: + """Quantise + FP8 GEMM, return raw fp16 output WITHOUT bias. + + The caller applies bias later (fused with gelu+quant). Only valid + when not calibrating (the fused FFN path is disabled during the + calibration warm-up pass). + """ + if self.calibrating or self.bias is None: + return self.forward(x) + if x.dtype != torch.float16: + x = x.to(torch.float16) + orig_shape = x.shape + x2d = x.reshape(-1, self.in_features) + if x2d.stride(0) != self.in_features or x2d.stride(1) != 1: + x2d = x2d.contiguous() + m = x2d.shape[0] + k, n = self.in_features, self.out_features + scale = self.act_scale.data + stream = torch.cuda.current_stream().cuda_stream + x_fp8 = torch.empty(m, k, dtype=_FP8, device=x2d.device) + out = torch.empty(m, n, dtype=torch.float16, device=x2d.device) + kern.quantize_fp8_static_fp16( + x2d.data_ptr(), x_fp8.data_ptr(), scale.data_ptr(), m * k, stream) + kern.fp8_gemm_descale_fp16( + x_fp8.data_ptr(), self.weight_fp8.data_ptr(), out.data_ptr(), + m, n, k, scale.data_ptr(), self.weight_scale.data_ptr(), stream) + return out.view(*orig_shape[:-1], n) + + def gemm_no_bias_from_fp8(self, x_fp8: torch.Tensor) -> torch.Tensor: + """FP8 GEMM from pre-quantised fp8 input, WITHOUT bias. + + Counterpart to ``gemm_no_bias`` when the input is already fp8 + (produced by ``bias_gelu_quant_fp16_fp8``). Caller applies bias + later (e.g. fused with gate + residual). + """ + if self.calibrating: + return self.forward(x_fp8) + orig_shape = x_fp8.shape + x2d = x_fp8.reshape(-1, self.in_features) + if x2d.stride(0) != self.in_features or x2d.stride(1) != 1: + x2d = x2d.contiguous() + m = x2d.shape[0] + k, n = self.in_features, self.out_features + scale = self.act_scale.data + stream = torch.cuda.current_stream().cuda_stream + out = torch.empty(m, n, dtype=torch.float16, device=x2d.device) + kern.fp8_gemm_descale_fp16( + x2d.data_ptr(), self.weight_fp8.data_ptr(), out.data_ptr(), + m, n, k, scale.data_ptr(), self.weight_scale.data_ptr(), stream) + return out.view(*orig_shape[:-1], n) + + def forward_from_fp8(self, x_fp8: torch.Tensor) -> torch.Tensor: + """FP8 GEMM from a pre-quantised fp8 input + bias (skip quantise). + + Counterpart to ``gemm_no_bias`` on the previous Linear: the input + was already quantised (by the fused bias+gelu+quant kernel), so we + only run the GEMM and the (non-fused) bias-add. + """ + if self.calibrating: + return self.forward(x_fp8) + orig_shape = x_fp8.shape + x2d = x_fp8.reshape(-1, self.in_features) + if x2d.stride(0) != self.in_features or x2d.stride(1) != 1: + x2d = x2d.contiguous() + m = x2d.shape[0] + k, n = self.in_features, self.out_features + scale = self.act_scale.data + stream = torch.cuda.current_stream().cuda_stream + out = torch.empty(m, n, dtype=torch.float16, device=x2d.device) + kern.fp8_gemm_descale_fp16( + x2d.data_ptr(), self.weight_fp8.data_ptr(), out.data_ptr(), + m, n, k, scale.data_ptr(), self.weight_scale.data_ptr(), stream) + if self.bias is not None: + if _has_add_bias_vec8 and (n & 7) == 0: + _fvk_extra.fp16_add_bias_vec8( + out.data_ptr(), self.bias.data_ptr(), m, n, stream) + else: + kern.add_bias_fp16(out.data_ptr(), self.bias.data_ptr(), + m, n, stream) + return out.view(*orig_shape[:-1], n) + + def gemm_from_fp8_ext(self, x_fp8: torch.Tensor, + ext_act_scale: torch.Tensor) -> torch.Tensor: + """FP8 GEMM from a pre-quantised fp8 input using an externally + supplied activation scale (fp32 [1]). + + Used by the fused ``ada_layernorm_quant_fp8_shared`` path, where + several sibling Linears (Q/K/V) share a single fp8-quantised + input built with ``shared_scale = max(scale_q, scale_k, scale_v)``. + The FP8 GEMM descales with the actual scale used at quantise time, + which is arithmetically correct regardless of self.act_scale. + + Includes bias. + """ + if self.calibrating: + raise RuntimeError("gemm_from_fp8_ext is only valid post-calibration") + orig_shape = x_fp8.shape + x2d = x_fp8.reshape(-1, self.in_features) + if x2d.stride(0) != self.in_features or x2d.stride(1) != 1: + x2d = x2d.contiguous() + m = x2d.shape[0] + k, n = self.in_features, self.out_features + stream = torch.cuda.current_stream().cuda_stream + out = torch.empty(m, n, dtype=torch.float16, device=x2d.device) + kern.fp8_gemm_descale_fp16( + x2d.data_ptr(), self.weight_fp8.data_ptr(), out.data_ptr(), + m, n, k, ext_act_scale.data_ptr(), + self.weight_scale.data_ptr(), stream) + if self.bias is not None: + if _has_add_bias_vec8 and (n & 7) == 0: + _fvk_extra.fp16_add_bias_vec8( + out.data_ptr(), self.bias.data_ptr(), m, n, stream) + else: + kern.add_bias_fp16(out.data_ptr(), self.bias.data_ptr(), + m, n, stream) + return out.view(*orig_shape[:-1], n) + + def gemm_from_fp8_ext_nobias(self, x_fp8: torch.Tensor, + ext_act_scale: torch.Tensor) -> torch.Tensor: + """Same as gemm_from_fp8_ext but WITHOUT the bias-add. + + For the fully-fused attention path where the Q/K bias is added + inside the downstream ``fp16_rmsnorm_rope_quant_int8_q`` / + ``fp16_rmsnorm_rope_quant_int8_k`` kernel (fused pre-norm) -- + avoids the separate ``add_bias_vec8`` kernel and its fp16 output + round-trip. + """ + if self.calibrating: + raise RuntimeError("gemm_from_fp8_ext_nobias is only valid post-calibration") + orig_shape = x_fp8.shape + x2d = x_fp8.reshape(-1, self.in_features) + if x2d.stride(0) != self.in_features or x2d.stride(1) != 1: + x2d = x2d.contiguous() + m = x2d.shape[0] + k, n = self.in_features, self.out_features + stream = torch.cuda.current_stream().cuda_stream + out = torch.empty(m, n, dtype=torch.float16, device=x2d.device) + kern.fp8_gemm_descale_fp16( + x2d.data_ptr(), self.weight_fp8.data_ptr(), out.data_ptr(), + m, n, k, ext_act_scale.data_ptr(), + self.weight_scale.data_ptr(), stream) + return out.view(*orig_shape[:-1], n) + def _is_fp8_target(module: nn.Module) -> bool: """Determine whether a Linear is suitable for FP8 replacement. diff --git a/flash_rt/models/minimax_remover/_fp8_manual_denoise.py b/flash_rt/models/minimax_remover/_fp8_manual_denoise.py new file mode 100644 index 00000000..02823157 --- /dev/null +++ b/flash_rt/models/minimax_remover/_fp8_manual_denoise.py @@ -0,0 +1,267 @@ +"""FlashRT -- MiniMax-Remover FP8 manual graph-capturable denoise pipeline. + +Replaces the diffusers ``MinimaxRemoverPipeline.__call__`` denoise loop +with a pointer-based implementation that mirrors ``_manual_denoise.py`` +(NVFP4) but calls the **installed FP8 block forwards** from +``_kern_block.install_fused_blocks``. + +Why a manual loop is needed for CUDA Graph capture: + * ``condition_embedder`` calls ``Timesteps`` which uses ``torch.arange`` + (a CPU op) -- breaks capture. + * ``FlowMatchEulerDiscreteScheduler.step`` mutates ``self.step_index`` + (a Python int) and does CPU indexing into ``sigmas`` -- breaks capture. + +This module: + 1. Pre-computes ALL per-step time embeddings + norm_out modulation + BEFORE capture (condition_embedder runs once per step, outside the + graph). + 2. Pre-computes RoPE freqs (fixed for a given latent shape). + 3. Pre-computes the Euler flow-matching ``dt`` values. + 4. Runs a manual denoise loop of pre-allocated buffer copies + the + installed FP8 transformer forward + an in-place Triton euler step -- + every operation is a kernel launch, fully CUDA-graph capturable. + 5. Captures the entire N-step loop as ONE graph and replays it on + subsequent calls with the same latent shape. + +The FP8 act_scales MUST be frozen (static calibration) before capture -- +the parent ``MiniMaxRemoverPipelineFP8`` guarantees this (mid-inference +freeze after step 1 on the first call). Capture therefore happens on the +second call; the first call runs the diffusers (calibration) path. + +-------------------------------------------------------------------- +Performance finding (measured on RTX 5060 Ti, 70-frame tennis clip) +-------------------------------------------------------------------- +Graph capture is **technically feasible and bit-correct** (with a +graph-safe attention backend, the captured graph reproduces the eager +result exactly). However it is **not a net win** for this workload: + + * The denoise loop is GPU-bound -- the kernels are large and the GPU + stays saturated, so Python/launch overhead is negligible + (graph replay saves only ~20 ms vs eager, both using triton_fp8). + * The fast default attention backend (``sage_fp8`` = SageAttention + QK-int8/PV-fp8) is **not** CUDA-graph safe -- it produces garbage + inside a captured graph (likely a CPU-syncing absmax reduction). + Switching to a graph-safe backend (``triton_fp8``/``triton_fp16``) + costs ~1.1 s, far more than the graph saves. + +Result: graph replay (triton_fp8) = 7.87 s vs the default sage_fp8 +eager path = 6.76 s (2nd call). The graph loses by ~1.1 s. + +The code is retained (gated behind ``FLASHRT_FP8_GRAPH=1``, default off) +because it is correct and would become worthwhile if a fast graph-safe +attention backend is added (e.g. FlashRT's vendored ``flash_rt_fa2``, +which is pointer-based and graph-safe but not built in this tree). + +No MiniMax-Remover imports: tensors + the loaded diffusers ``pipe`` only. +""" + +import logging +import os + +import torch +from einops import rearrange +from diffusers.utils.torch_utils import randn_tensor + +from ._kernels import (ada_layernorm_fp16_io, euler_step_inplace, mask_mul, + latent_normalize, latent_denormalize) + +logger = logging.getLogger(__name__) + + +def transformer_forward_fp8(transformer, hidden_states, tproj_step, + mod_out_step, rotary_emb, eps): + """FP8 transformer forward with pre-computed time projection + norm_out mod. + + Calls the installed FP8 block forwards (``block(hs, tproj, rope)``). + Avoids ``condition_embedder`` (torch.arange) and uses ``ada_layernorm`` + for norm_out, so the whole forward is CUDA-graph capturable. + + Args: + transformer: Transformer3DModel with FP8-patched blocks + attention. + hidden_states: [B, 3*C, T, H, W] concat latent (fp16). + tproj_step: [1, 6, D] pre-computed time projection (unflattened). + mod_out_step: [2, D] fp32 (shift, scale) for norm_out. + rotary_emb: pre-computed RoPE freqs. + eps: layer-norm epsilon. + Returns: + [B, C_out, T, H, W] noise prediction (fp16). + """ + B, C_in, T, H, W = hidden_states.shape + p_t, p_h, p_w = transformer.config.patch_size + post_t, post_h, post_w = T // p_t, H // p_h, W // p_w + + hs = transformer.patch_embedding(hidden_states) + hs = hs.flatten(2).transpose(1, 2) + + for block in transformer.blocks: + hs = block(hs, tproj_step, rotary_emb) + + S, D = hs.shape[1], hs.shape[2] + hs_2d = hs.contiguous().view(S, D) + # norm_out: original is (FP32LayerNorm(hs) * (1+scale) + shift).type_as; + # ada_layernorm_fp16_io is the fp32-stat reference-equivalent single kernel + # (same kernel used by every block's norm1/norm2). mod_out_step[0]=shift, + # mod_out_step[1]=scale. + hs_2d = ada_layernorm_fp16_io(hs_2d, mod_out_step[1], mod_out_step[0], eps) + hs = transformer.proj_out(hs_2d.view(1, S, D)) + + hs = hs.reshape(B, post_t, post_h, post_w, p_t, p_h, p_w, -1) + hs = hs.permute(0, 7, 1, 4, 2, 5, 3, 6) + return hs.flatten(6, 7).flatten(4, 5).flatten(2, 3) + + +class FP8ManualDenoise: + """Manual graph-capturable denoise for the FP8 (W8A8) path. + + Owned by ``MiniMaxRemoverPipelineFP8``. ``denoise(...)`` runs the full + N-step loop, capturing a CUDA Graph on the first invocation for a given + latent shape and replaying it thereafter. FP8 scales must be frozen + before the first ``denoise`` call. + """ + + def __init__(self, pipe, transformer): + self.pipe = pipe + self.transformer = transformer + self.eps = float(transformer.config.eps) + self._dtype = next(transformer.parameters()).dtype + self._vae_dtype = next(pipe.vae.parameters()).dtype + # Per-shape cache: (graph, lat_buf, masked_buf, masks_buf, + # tproj_all, mod_out, dt_all, rotary_emb) + self._graphs = {} + + # ------------------------------------------------------------------ # + # Static (per-shape, per-step) pre-computation -- runs OUTSIDE graph. # + # ------------------------------------------------------------------ # + def _precompute_static(self, latents, num_steps): + """Pre-compute time embeddings, norm_out modulation, RoPE, dt. + + These are identical for every call with the same (latent shape, + num_steps), so they are cached per shape key. + """ + device = latents.device + scheduler = self.pipe.scheduler + scheduler.set_timesteps(num_steps, device=device) + timesteps = scheduler.timesteps + sigmas = scheduler.sigmas + dt_all = [float(sigmas[i + 1] - sigmas[i]) for i in range(num_steps)] + + tr = self.transformer + D = tr.scale_shift_table.shape[-1] + temb_all, tproj_all = [], [] + mod_out = torch.empty(num_steps, 2, D, dtype=torch.float32, + device=device) + with torch.no_grad(): + for i in range(num_steps): + t_step = timesteps[i:i + 1] + temb_i, tproj_i = tr.condition_embedder(t_step) + tproj_i = tproj_i.unflatten(1, (6, -1)) + temb_all.append(temb_i) + tproj_all.append(tproj_i) + temb_f = temb_i.float().unsqueeze(1) + mod_out[i] = (tr.scale_shift_table + temb_f).squeeze(0) + + rotary_emb = tr.rope(latents) + return tproj_all, mod_out, dt_all, rotary_emb + + # ------------------------------------------------------------------ # + # The graph-capturable N-step loop. # + # ------------------------------------------------------------------ # + def _denoise_loop_body(self, lat_buf, masked_buf, masks_buf, concat_buf, + tproj_all, mod_out, dt_all, rotary_emb, num_steps): + C = lat_buf.shape[1] + tr = self.transformer + eps = self.eps + for step in range(num_steps): + concat_buf[:, :C].copy_(lat_buf) + concat_buf[:, C:2 * C].copy_(masked_buf) + concat_buf[:, 2 * C:3 * C].copy_(masks_buf) + noise_pred = transformer_forward_fp8( + tr, concat_buf, tproj_all[step], mod_out[step], + rotary_emb, eps) + euler_step_inplace(lat_buf, noise_pred, dt_all[step]) + + def _capture_graph(self, latents, masked_latents, masks_latents, + tproj_all, mod_out, dt_all, rotary_emb, num_steps): + """Capture the full N-step denoise loop as one CUDA Graph.""" + device = latents.device + C = latents.shape[1] + B, _, T, H, W = latents.shape + dtype = latents.dtype + + lat_buf = latents.clone() + masked_buf = masked_latents.clone() + masks_buf = masks_latents.clone() + concat_buf = torch.empty(B, 3 * C, T, H, W, dtype=dtype, device=device) + + def denoise(): + self._denoise_loop_body( + lat_buf, masked_buf, masks_buf, concat_buf, + tproj_all, mod_out, dt_all, rotary_emb, num_steps) + + # Warmup on a side stream to compile all kernels / init cuBLASLt + # workspaces before capture. The FP8 scales are already frozen, so + # the warmup runs with the exact same kernels as capture/replay. + s = torch.cuda.Stream() + n_warmup = int(os.environ.get("FLASHRT_FP8_GRAPH_WARMUP", "1")) + with torch.cuda.stream(s): + for _ in range(n_warmup): + denoise() + torch.cuda.current_stream().wait_stream(s) + torch.cuda.synchronize() + + # Reset the latent buffer to the caller's input before capture (the + # warmup mutated it); the capture pass will produce the final result. + lat_buf.copy_(latents) + + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph, stream=s): + denoise() + + logger.info("MiniMax-Remover FP8: CUDA Graph captured for shape %s " + "(%d steps, warmup=%d)", tuple(latents.shape), + num_steps, n_warmup) + return (graph, lat_buf, masked_buf, masks_buf, + tproj_all, mod_out, dt_all, rotary_emb) + + # ------------------------------------------------------------------ # + # Public entry: run the denoise, capture-or-replay per shape. # + # ------------------------------------------------------------------ # + def denoise(self, latents, masked_latents, masks_latents, num_steps, + use_graph=True): + """Run the N-step denoise; capture+replay a graph when use_graph. + + Returns the final latents tensor (same shape/dtype as ``latents``). + """ + shape_key = tuple(latents.shape) + (num_steps,) + entry = self._graphs.get(shape_key) if use_graph else None + + if entry is None: + tproj_all, mod_out, dt_all, rotary_emb = self._precompute_static( + latents, num_steps) + if use_graph: + entry = self._capture_graph( + latents, masked_latents, masks_latents, + tproj_all, mod_out, dt_all, rotary_emb, num_steps) + self._graphs[shape_key] = entry + else: + # Eager manual path (no graph) -- still avoids condition_embedder + # / scheduler CPU ops; useful for PSNR validation vs graph. + C = latents.shape[1] + concat_buf = torch.empty( + latents.shape[0], 3 * C, *latents.shape[2:], + dtype=latents.dtype, device=latents.device) + lat_buf = latents.clone() + self._denoise_loop_body( + lat_buf, masked_latents, masks_latents, concat_buf, + tproj_all, mod_out, dt_all, rotary_emb, num_steps) + return lat_buf + + (graph, lat_buf, masked_buf, masks_buf, + tproj_all, mod_out, dt_all, rotary_emb) = entry + # Copy this call's inputs into the captured buffers, then replay. + lat_buf.copy_(latents) + masked_buf.copy_(masked_latents) + masks_buf.copy_(masks_latents) + graph.replay() + torch.cuda.synchronize() + return lat_buf.clone() diff --git a/flash_rt/models/minimax_remover/_fp8_pipeline.py b/flash_rt/models/minimax_remover/_fp8_pipeline.py index f246b352..c6a60a21 100644 --- a/flash_rt/models/minimax_remover/_fp8_pipeline.py +++ b/flash_rt/models/minimax_remover/_fp8_pipeline.py @@ -15,10 +15,14 @@ import os import torch +from einops import rearrange +from diffusers.utils.torch_utils import randn_tensor logger = logging.getLogger(__name__) from flash_rt.models.minimax_remover._utils import load_fp8_kernels +from flash_rt.models.minimax_remover._kernels import mask_mul +from flash_rt.models.minimax_remover._fp8_manual_denoise import FP8ManualDenoise def _import_runtime_fp8(): @@ -96,20 +100,124 @@ def __init__(self, pipe, num_inference_steps=12, fp8_target="all", self._orig_pipe_call = self.pipe.__call__ + # Manual graph-capturable denoise (used once calibrated + when + # FLASHRT_FP8_GRAPH=1). Lazily captures a CUDA Graph per latent shape. + self._graph_denoise = FP8ManualDenoise(self.pipe, self.transformer) + # Transformer compute dtype. ``next(transformer.parameters())`` is + # unreliable here because scale_shift_table / time_embedder are kept + # in fp32 (via _keep_in_fp32_modules). The diffusers reference path + # hardcodes fp16 (bf16 only when use_bf16). + self._dtype = torch.bfloat16 if use_bf16 else torch.float16 + self._vae_dtype = next(self.pipe.vae.parameters()).dtype + @torch.no_grad() def __call__(self, *args, **kwargs): - """Run the wrapped pipe, calibrating FP8 scales on the first call.""" + """Run the wrapped pipe, calibrating FP8 scales on the first call. + + On the first call, a one-shot forward hook on the transformer + freezes the FP8 act_scales immediately after the FIRST denoise + step completes. This lets steps 2..N (and the fused FFN epilogue + kernel) run with static scales, so a single-call invocation + benefits from the fused path instead of only multi-call ones. + The cost is a single CPU sync (~1 ms) after step 1. + + When ``FLASHRT_FP8_GRAPH=1`` and scales are frozen (call 2+), the + denoise loop runs via the manual graph-capturable path + (``_manual_call`` -> ``FP8ManualDenoise``). The first call always + uses the diffusers path (calibration); the graph is captured on + the second call and replayed thereafter. + """ + use_graph = os.environ.get("FLASHRT_FP8_GRAPH", "0") == "1" if not self._calibrated: logger.info("MiniMax-Remover FP8: calibration mode " - "(first call, dynamic FP8 + amax accumulation)") + "(first call, dynamic FP8 + amax accumulation; " + "freezes after step 1)") self._set_calibration(True) - - result = self._orig_pipe_call(*args, **kwargs) - - if not self._calibrated: - n = self._freeze_calibration() - self._calibrated = True - logger.info("MiniMax-Remover FP8: calibration done, " - "froze %d static act_scales (margin=%.2f)", - n, self.calib_margin) + # One-shot hook: freeze after the first transformer forward. + fired = [False] + + def _freeze_after_step1(_module, _inp, _out): + if fired[0]: + return + fired[0] = True + n = self._freeze_calibration() + self._calibrated = True + logger.info("MiniMax-Remover FP8: mid-inference freeze " + "after step 1 — %d act_scales frozen " + "(margin=%.2f); steps 2+ now use static FP8 " + "+ fused FFN epilogue", n, self.calib_margin) + + handle = self.transformer.register_forward_hook( + _freeze_after_step1) + try: + result = self._orig_pipe_call(*args, **kwargs) + finally: + handle.remove() + elif use_graph: + # Frozen scales + graph requested: manual graph-capturable path. + result = self._manual_call(*args, use_graph=True, **kwargs) + elif os.environ.get("FLASHRT_FP8_EAGER_MANUAL", "1") == "1": + # Steady-state: eager manual denoise (avoids the per-step + # torch.cat of [latents, masked, masks] and the scheduler.step + # CPU sync of the diffusers path). masked/masks latents are + # constant across steps; _denoise_loop_body copies only the + # changing latents slice into a persistent concat buffer. + result = self._manual_call(*args, use_graph=False, **kwargs) + else: + result = self._orig_pipe_call(*args, **kwargs) return result + + @torch.no_grad() + def _manual_call(self, images, masks, num_frames, height, width, + num_inference_steps=12, generator=None, iterations=16, + output_type="np", use_graph=False): + """Manual encode + graph-denoise + decode (mirrors the diffusers + ``MinimaxRemoverPipeline.__call__`` but replaces the denoise loop + with the CUDA-graph-capturable ``FP8ManualDenoise``). Requires + frozen FP8 scales (caller guarantees calibration is done). + """ + pipe = self.pipe + device = self.transformer.device + + pipe.scheduler.set_timesteps(num_inference_steps, device=device) + num_channels_latents = 16 + vsft = pipe.vae_scale_factor_temporal + vsfs = pipe.vae_scale_factor_spatial + num_latent_frames = (num_frames - 1) // vsft + 1 + shape = (1, num_channels_latents, num_latent_frames, + height // vsfs, width // vsfs) + latents = randn_tensor(shape, generator=generator, device=device, + dtype=self._dtype) + + masks_t = pipe.expand_masks(masks, iterations) + masks_t = pipe.resize(masks_t, height, width).to(device).to(self._vae_dtype) + masks_t[masks_t > 0] = 1 + images_t = rearrange(images, "f h w c -> c f h w") + images_t = pipe.resize(images_t[None, ...], height, width).to(device).to(self._vae_dtype) + masked_images = mask_mul(images_t, masks_t) + + latents_mean = (torch.tensor(pipe.vae.config.latents_mean) + .view(1, pipe.vae.config.z_dim, 1, 1, 1) + .to(device, self._vae_dtype)) + latents_std = 1.0 / torch.tensor(pipe.vae.config.latents_std).view( + 1, pipe.vae.config.z_dim, 1, 1, 1).to(device, self._vae_dtype) + + masked_latents = pipe.vae.encode(masked_images.to(self._vae_dtype)).latent_dist.mode() + masks_latents = pipe.vae.encode((2 * masks_t - 1.0).to(self._vae_dtype)).latent_dist.mode() + # Per-channel normalize (matches diffusers exactly). Done outside the + # graph; the latent_normalize() Triton helper collapses latents_std to + # a scalar via .max() which is wrong for per-channel stats. + masked_latents = ((masked_latents - latents_mean) * latents_std).to(self._dtype) + masks_latents = ((masks_latents - latents_mean) * latents_std).to(self._dtype) + + result_latents = self._graph_denoise.denoise( + latents, masked_latents, masks_latents, num_inference_steps, + use_graph=use_graph) + + result_latents = (result_latents.to(self._vae_dtype) / latents_std + + latents_mean) + video = pipe.vae.decode(result_latents, return_dict=False)[0] + video = pipe.video_processor.postprocess_video(video, output_type=output_type) + + from diffusers.pipelines.wan.pipeline_output import WanPipelineOutput + return WanPipelineOutput(frames=video) diff --git a/flash_rt/models/minimax_remover/_kern_block.py b/flash_rt/models/minimax_remover/_kern_block.py index cd5afe8f..1ad56b29 100644 --- a/flash_rt/models/minimax_remover/_kern_block.py +++ b/flash_rt/models/minimax_remover/_kern_block.py @@ -41,6 +41,18 @@ from ._kernels import ada_layernorm_fp16_io, rms_norm_fp32stat, gate_mul_residual_bcast from ._attention import install_attention +# MiniMax-Remover fused FFN epilogue kernel (bias+gelu+quant → fp8). +# Opt-in module; if absent the FFN path falls back to the 3-kernel +# sequence (bias-add + gelu-inplace + quantise). +_fvk = None +try: + from flash_rt import flash_rt_minimax_remover as _fvk +except ImportError: + try: + import flash_rt_minimax_remover as _fvk + except ImportError: + pass + # Backward-compat alias: the FP8 pipeline (``_fp8_pipeline``) installs the # kernel attention processor via this name. The single source of truth for # attention dispatch lives in ``_attention`` and is shared by both paths. @@ -91,6 +103,53 @@ def _ada_norm_flashrt_fp16(self_hs, scale_v, shift_v, S, D): out.data_ptr(), S, D, eps, stream_of()) return out + _has_bgr = (_fvk is not None + and hasattr(_fvk, "fp16_bias_gate_residual_bcast") + and os.environ.get("FLASHRT_DISABLE_BIAS_GATE", "0") != "1") + + # Fused adaLN + fp8 quantise kernel — feeds Q/K/V from a shared fp8 + # tensor built with max(act_scale_q, act_scale_k, act_scale_v). Saves + # three activation-quantise passes and one full fp16 read of norm1. + _fuse_ada_qkv = (_fvk is not None + and hasattr(_fvk, "fp16_ada_layernorm_quant_fp8") + and os.environ.get("FLASHRT_DISABLE_ADA_QKV", "0") != "1") + + # Fused norm2 + fp8 quantise — feeds FFN proj0 from fp8 directly, + # eliminating the intermediate fp16 [S,D] write + read + quantise. + _fuse_norm2_ffn = (_fvk is not None + and hasattr(_fvk, "fp16_ada_layernorm_quant_fp8") + and os.environ.get("FLASHRT_DISABLE_NORM2_FFN", "0") != "1") + + def _gate_residual_apply(hs, x_no_bias, bias, gate, S, D): + """residual += (x + bias) * gate[D] — one fused kernel if available, + else falls back to add_bias_fp16 + gate_mul_residual_bcast.""" + if _has_bgr and bias is not None and (D & 7) == 0: + gate_fp16 = gate.to(_FP16).contiguous().view(D) + _fvk.fp16_bias_gate_residual_bcast( + x_no_bias.data_ptr(), bias.data_ptr(), + gate_fp16.data_ptr(), hs.data_ptr(), + S, D, stream_of()) + else: + if bias is not None: + kern.add_bias_fp16(x_no_bias.data_ptr(), bias.data_ptr(), + S, D, stream_of()) + gate_mul_residual_bcast(hs, x_no_bias, gate.view(D)) + + def _get_qkv_shared_scale(block): + """Cache & return the fp32[1] max(to_q, to_k, to_v).act_scale for + the attn1 sub-module of `block`. Persists across denoise steps + (scales are frozen after calibration).""" + cached = getattr(block, "_flashrt_qkv_shared_scale", None) + if cached is not None: + return cached + attn = block.attn1 + s = torch.stack([attn.to_q.act_scale.view(1), + attn.to_k.act_scale.view(1), + attn.to_v.act_scale.view(1)]).max().view(1) + s = s.to(torch.float32).contiguous() + block._flashrt_qkv_shared_scale = s + return s + def block_forward(self, hidden_states, temb, rotary_emb): B, S, D = hidden_states.shape # Ensure contiguity at entry (first block truly copies; subsequent blocks are no-ops) @@ -99,27 +158,121 @@ def block_forward(self, hidden_states, temb, rotary_emb): (shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa) = (self.scale_shift_table + temb.float()).chunk(6, dim=1) - if norm_mode == "fp16": - norm1_out = _ada_norm_flashrt_fp16(hs, scale_msa, shift_msa, S, D) + # ── norm1 → attn ──────────────────────────────────────── + attn = self.attn1 + to_out0 = attn.to_out[0] + _fuse_qkv = (_fuse_ada_qkv + and norm_mode != "fp16" + and hasattr(attn.to_q, "gemm_from_fp8_ext") + and hasattr(attn.to_k, "gemm_from_fp8_ext") + and hasattr(attn.to_v, "gemm_from_fp8_ext") + and not getattr(attn.to_q, "calibrating", False) + and (D & 7) == 0) + _fuse_attn = (_has_bgr + and hasattr(to_out0, "gemm_no_bias") + and getattr(to_out0, "bias", None) is not None + and not getattr(to_out0, "calibrating", False) + and (D & 7) == 0) + + if _fuse_qkv: + shared_scale = _get_qkv_shared_scale(self) + # Ensure fp32 contiguous scale/shift vectors for the kernel. + scale_f = scale_msa.contiguous().to(torch.float32).view(D) + shift_f = shift_msa.contiguous().to(torch.float32).view(D) + norm1_fp8 = torch.empty(S, D, dtype=torch.float8_e4m3fn, + device=hs.device) + _fvk.fp16_ada_layernorm_quant_fp8( + hs.data_ptr(), scale_f.data_ptr(), shift_f.data_ptr(), + shared_scale.data_ptr(), norm1_fp8.data_ptr(), + S, D, eps, stream_of()) + attn_out = attn(hidden_states=hs.view(1, S, D), + rotary_emb=rotary_emb, + no_out_bias=_fuse_attn, + fp8_hidden=norm1_fp8, + fp8_scale=shared_scale).view(S, D) else: - norm1_out = _ada_norm(hs, scale_msa, shift_msa, S, D) - attn_out = self.attn1(hidden_states=norm1_out.view(1, S, D), rotary_emb=rotary_emb).view(S, D) - # Broadcast gate[D] (avoids [S,D] expand copy); fp16 in-place - gate_mul_residual_bcast(hs, attn_out, gate_msa.view(D)) + if norm_mode == "fp16": + norm1_out = _ada_norm_flashrt_fp16(hs, scale_msa, shift_msa, S, D) + else: + norm1_out = _ada_norm(hs, scale_msa, shift_msa, S, D) + attn_out = attn(hidden_states=norm1_out.view(1, S, D), + rotary_emb=rotary_emb, + no_out_bias=_fuse_attn).view(S, D) - if norm_mode == "fp16": - norm2_out = _ada_norm_flashrt_fp16(hs, c_scale_msa, c_shift_msa, S, D) + if _fuse_attn: + _gate_residual_apply(hs, attn_out, to_out0.bias, gate_msa, S, D) else: - norm2_out = _ada_norm(hs, c_scale_msa, c_shift_msa, S, D) - n2_3d = norm2_out.view(1, S, D) + gate_mul_residual_bcast(hs, attn_out, gate_msa.view(D)) + + proj0 = self.ffn.net[0].proj + proj1 = self.ffn.net[2] + _can_fuse = ( + _fvk is not None + and hasattr(proj0, "gemm_no_bias") + and hasattr(proj1, "forward_from_fp8") + and not getattr(proj1, "calibrating", False) + and getattr(proj0, "bias", None) is not None + and (proj0.out_features & 3) == 0) + _fuse_ffn_down = (_has_bgr and _can_fuse + and hasattr(proj1, "gemm_no_bias_from_fp8") + and getattr(proj1, "bias", None) is not None + and (D & 7) == 0) + + _do_norm2_fp8 = (_fuse_norm2_ffn and _can_fuse + and hasattr(proj0, "gemm_no_bias_from_fp8") + and not getattr(proj0, "calibrating", False) + and (D & 7) == 0) + if gelu_mode == "torch": - ff_out = self.ffn(n2_3d).view(S, D) + if norm_mode == "fp16": + norm2_out = _ada_norm_flashrt_fp16(hs, c_scale_msa, c_shift_msa, S, D) + else: + norm2_out = _ada_norm(hs, c_scale_msa, c_shift_msa, S, D) + ff_out = self.ffn(norm2_out.view(1, S, D)).view(S, D) + gate_mul_residual_bcast(hs, ff_out, c_gate_msa.view(D)) + return hs.view(1, S, D) + + if _do_norm2_fp8: + c_scale_f = c_scale_msa.contiguous().to(torch.float32).view(D) + c_shift_f = c_shift_msa.contiguous().to(torch.float32).view(D) + norm2_fp8 = torch.empty(S, D, dtype=torch.float8_e4m3fn, + device=hs.device) + _fvk.fp16_ada_layernorm_quant_fp8( + hs.data_ptr(), c_scale_f.data_ptr(), c_shift_f.data_ptr(), + proj0.act_scale.data_ptr(), norm2_fp8.data_ptr(), + S, D, eps, stream_of()) + raw = proj0.gemm_no_bias_from_fp8(norm2_fp8) + elif _can_fuse: + if norm_mode == "fp16": + norm2_out = _ada_norm_flashrt_fp16(hs, c_scale_msa, c_shift_msa, S, D) + else: + norm2_out = _ada_norm(hs, c_scale_msa, c_shift_msa, S, D) + raw = proj0.gemm_no_bias(norm2_out.view(1, S, D)) else: - up = self.ffn.net[0].proj(n2_3d) + if norm_mode == "fp16": + norm2_out = _ada_norm_flashrt_fp16(hs, c_scale_msa, c_shift_msa, S, D) + else: + norm2_out = _ada_norm(hs, c_scale_msa, c_shift_msa, S, D) + up = proj0(norm2_out.view(1, S, D)) inner = up.shape[-1] _gelu_fn = kern.gelu_inplace if up.dtype == torch.bfloat16 else kern.gelu_inplace_fp16 _gelu_fn(up.data_ptr(), S * inner, stream_of()) - ff_out = self.ffn.net[2](up).view(S, D) + ff_out = proj1(up).view(S, D) + gate_mul_residual_bcast(hs, ff_out, c_gate_msa.view(D)) + return hs.view(1, S, D) + + inner = raw.shape[-1] + up_fp8 = torch.empty( + S, inner, dtype=torch.float8_e4m3fn, device=raw.device) + _fvk.bias_gelu_quant_fp16_fp8( + raw.data_ptr(), proj0.bias.data_ptr(), + up_fp8.data_ptr(), proj1.act_scale.data_ptr(), + S, inner, stream_of()) + if _fuse_ffn_down: + ff_out = proj1.gemm_no_bias_from_fp8(up_fp8).view(S, D) + _gate_residual_apply(hs, ff_out, proj1.bias, c_gate_msa, S, D) + return hs.view(1, S, D) + ff_out = proj1.forward_from_fp8(up_fp8).view(S, D) gate_mul_residual_bcast(hs, ff_out, c_gate_msa.view(D)) return hs.view(1, S, D) diff --git a/flash_rt/models/minimax_remover/_vae_nvfp4.py b/flash_rt/models/minimax_remover/_vae_nvfp4.py new file mode 100644 index 00000000..6ee5d4cb --- /dev/null +++ b/flash_rt/models/minimax_remover/_vae_nvfp4.py @@ -0,0 +1,569 @@ +"""FlashRT — MiniMax-Remover WanVAE NVFP4 (W4A4) conv3d integration. + +Replaces eligible 3×3×3 WanCausalConv3d layers in the WanVAE with an +NVFP4 (W4A4) path that uses: + + 1. **FlashRT fp16_quant_nvfp4_ndhwc** (novel CUDA kernel in this repo): + fp16 NCDHW → NVFP4 packed + UE4M3 block-scale (NDHWC layout). + Fuses the layout conversion + quantization into one pass. + + 2. **motus_fp4_conv3d_v19sfb_ncdhw_res_bf16out** (existing SM120 kernel): + NVFP4 W4A4 implicit-GEMM conv3d with bias + optional residual. + Uses mma.sync.kind::mxf4nvf4 (e2m1 × e2m1, UE4M3 block scales). + +Weight is pre-quantized once at install time (bf16 → NVFP4 via +``quantize_bf16_to_nvfp4``). Activations are quantized dynamically +per call (fp16 → FP4, on-GPU, no CPU sync). + +Eligibility: 3×3×3 WanCausalConv3d with Ci % 96 == 0 (WanVAE channels +96/192/384 all qualify). The FP4 path is installed ADDITIVELY over +the existing FP8 path — eligible layers switch to FP4, ineligible +layers stay on FP8. + +Env knobs: + FLASHRT_NVFP4_VAE=1 enable (default ON) + FLASHRT_NVFP4_VAE_DECODE_ONLY=0 only patch decoder (default 0 = enc+dec) + FLASHRT_NVFP4_VAE_MIN_CI=192 minimum Ci for FP4 (default 192) +""" +from __future__ import annotations + +import logging +import os +from typing import Optional, Dict + +import torch +import torch.nn as nn + +logger = logging.getLogger(__name__) + +_FP16 = torch.float16 +_BF16 = torch.bfloat16 + +# Direction 3: block-level fused norm+silu+NVFP4-quant (single kernel). +# Default ON (FLASHRT_NVFP4_FUSED_NORMQUANT=1): the fused kernel emits the +# FP4 activation directly from the norm (no fp16 round-trip) and the conv +# reuses the rolling 2-frame FP4 cache. End-to-end PSNR 35.23 dB vs 35.11 +# baseline. The actual gate is read in install_vae_nvfp4() below. +# Set FLASHRT_NVFP4_FUSED_NORMQUANT=0 to fall back to the separate +# norm→fp16→quant path (Direction-2 cache only). + +# Lazy-loaded kernel modules +_fvk = None # flash_rt_minimax_remover (our new kernels) +_frk = None # flash_rt_kernels (motus FP4 conv3d + weight quant) + + +def _load_kernels(): + global _fvk, _frk + if _fvk is not None: + return True + try: + from flash_rt import flash_rt_minimax_remover as _fvk + except ImportError: + try: + import flash_rt_minimax_remover as _fvk + except ImportError: + logger.error("[nvfp4_vae] flash_rt_minimax_remover not found") + return False + try: + from flash_rt import flash_rt_kernels as _frk + except ImportError: + import flash_rt_kernels as _frk + if not hasattr(_frk, 'quantize_bf16_to_nvfp4'): + logger.error("[nvfp4_vae] quantize_bf16_to_nvfp4 not in flash_rt_kernels") + return False + if not hasattr(_fvk, 'nvfp4_conv3d_ndhwc_fp16out'): + logger.error("[nvfp4_vae] nvfp4_conv3d_ndhwc_fp16out not in flash_rt_minimax_remover") + return False + return True + + +# ── Weight pre-quantization (one-time at install) ── + +def _quantize_weight_nvfp4(conv_weight: torch.Tensor): + """Quantize a 3×3×3 conv3d weight [Co,Ci,3,3,3] fp16 to NVFP4. + + Returns (w_fp4 [Co,3,3,3,Ci/2] uint8, w_sf [Co,3,3,3,Ci/16] uint8). + """ + Co, Ci = conv_weight.shape[0], conv_weight.shape[1] + # Permute to [Co, kT, kH, kW, Ci] and flatten to [Co*27, Ci] + w_bf16 = conv_weight.to(_BF16).permute(0, 2, 3, 4, 1).contiguous() + rows = Co * 27 + w_2d = w_bf16.view(rows, Ci) + + w_fp4 = torch.empty((rows, Ci // 2), dtype=torch.uint8, device=w_2d.device) + w_sf = torch.empty((rows, Ci // 16), dtype=torch.uint8, device=w_2d.device) + _frk.quantize_bf16_to_nvfp4( + w_2d.data_ptr(), w_fp4.data_ptr(), w_sf.data_ptr(), + rows, Ci, 0) + torch.cuda.synchronize(w_2d.device) + + return (w_fp4.view(Co, 3, 3, 3, Ci // 2).contiguous(), + w_sf.view(Co, 3, 3, 3, Ci // 16).contiguous()) + + +# ── Activation quantization (per-call, dynamic) ── + +def _quant_act_nvfp4(x: torch.Tensor, B: int, C: int, T: int, H: int, W: int): + """Quantize fp16 [B,C,T,H,W] → NVFP4 NDHWC flat + SF flat. + + Automatically detects channels-last 3D layout and uses the CL kernel + variant to avoid a contiguous() copy. + """ + n = B * T * H * W + fp4 = torch.empty(n * (C // 2), dtype=torch.uint8, device=x.device) + sf = torch.empty(n * (C // 16), dtype=torch.uint8, device=x.device) + s = torch.cuda.current_stream().cuda_stream + if x.is_contiguous(memory_format=torch.channels_last_3d): + # Channels-last: use CL kernel, no copy needed + rc = _fvk.fp16_quant_nvfp4_cl_ndhwc( + x.data_ptr(), fp4.data_ptr(), sf.data_ptr(), + B, C, T, H, W, s) + else: + x_c = x.contiguous(memory_format=torch.contiguous_format) + rc = _fvk.fp16_quant_nvfp4_ndhwc( + x_c.data_ptr(), fp4.data_ptr(), sf.data_ptr(), + B, C, T, H, W, s) + if rc != 0: + raise RuntimeError(f"[nvfp4_vae] quant_nvfp4 rc={rc} " + f"shape=({B},{C},{T},{H},{W})") + return fp4, sf + + +# ── FP4 conv3d forward ── + +def _nvfp4_conv3d_forward(self, x: torch.Tensor, cache_x=None): + """NVFP4 W4A4 conv3d forward with FP4 cache (Direction 2). + + Caches the quantized FP4 activation for the next call's cache, + eliminating per-call cache quantization. + """ + B, Ci, T_new, H, W = x.shape + Co = self._nvfp4_w.shape[0] + s = torch.cuda.current_stream().cuda_stream + + # Quantize new activation + new_fp4, new_sf = _quant_act_nvfp4(x, B, Ci, T_new, H, W) + + # FP4 cache: reuse stored FP4 from previous call if available (Direction 2) + # Set FLASHRT_NVFP4_NO_CACHE=1 to disable (always quantize per call) + use_fp4_cache = os.environ.get('FLASHRT_NVFP4_NO_CACHE', '0') != '1' + stored_fp4 = getattr(self, '_nvfp4_stored_fp4', None) if use_fp4_cache else None + stored_sf = getattr(self, '_nvfp4_stored_sf', None) if use_fp4_cache else None + stored_T = getattr(self, '_nvfp4_stored_T', 0) if use_fp4_cache else 0 + + if stored_fp4 is not None and stored_T >= 2: + # Use stored 2-frame cache directly (already the right format) + cache_fp4 = stored_fp4 + cache_sf = stored_sf + elif stored_fp4 is not None and stored_T == 1: + # Previous call had T_new=1: pad [zero, prev_frame] + cache_fp4 = torch.zeros(B * _CACHE_T * H * W * (Ci // 2), dtype=torch.uint8, device=x.device) + cache_sf = torch.zeros(B * _CACHE_T * H * W * (Ci // 16), dtype=torch.uint8, device=x.device) + off = B * H * W * (Ci // 2) + cache_fp4[off:off + stored_fp4.numel()] = stored_fp4 + off_sf = B * H * W * (Ci // 16) + cache_sf[off_sf:off_sf + stored_sf.numel()] = stored_sf + elif cache_x is not None and cache_x.shape[2] >= 1: + # Fallback: quantize fp16 cache per call (same as non-fused path) + cache_2 = cache_x[:, :, -2:] + if cache_2.shape[2] < 2: + pad = torch.zeros(B, Ci, 2 - cache_2.shape[2], H, W, + dtype=x.dtype, device=x.device) + cache_2 = torch.cat([pad, cache_2], dim=2) + if not cache_2.is_contiguous(): + cache_2 = cache_2.contiguous( + memory_format=torch.channels_last_3d + if cache_2.is_contiguous(memory_format=torch.channels_last_3d) + else torch.contiguous_format) + cache_fp4, cache_sf = _quant_act_nvfp4(cache_2, B, Ci, 2, H, W) + else: + cache_fp4 = torch.zeros(B * _CACHE_T * H * W * (Ci // 2), dtype=torch.uint8, device=x.device) + cache_sf = torch.zeros(B * _CACHE_T * H * W * (Ci // 16), dtype=torch.uint8, device=x.device) + + # Purpose-built kernel: fp16 NDHWC output + M_total = B * T_new * H * W + out_ndhwc = torch.empty(M_total * Co, dtype=_FP16, device=x.device) + bias_ptr = self.bias.data_ptr() if self.bias is not None else 0 + + rc = _fvk.nvfp4_conv3d_ndhwc_fp16out( + cache_fp4.data_ptr(), new_fp4.data_ptr(), + self._nvfp4_w.data_ptr(), + cache_sf.data_ptr(), new_sf.data_ptr(), + self._nvfp4_sf.data_ptr(), + out_ndhwc.data_ptr(), bias_ptr, + B, _CACHE_T, T_new, H, W, Ci, Co, 1.0, s) + if rc != 0: + logger.warning("[nvfp4_vae] conv3d kernel rc=%d, falling back to cuDNN", rc) + padding = list(self._padding) + if cache_x is not None and self._padding[4] > 0: + cx = cache_x.to(x.device) + x_cat = torch.cat([cx, x], dim=2) + padding[4] -= cx.shape[2] + else: + x_cat = x + x_pad = torch.nn.functional.pad(x_cat, padding) + return torch.nn.functional.conv3d( + x_pad, self.weight, self.bias, self.stride, + self.padding, self.dilation, self.groups) + + # Store rolling 2-frame cache for next call (Direction 2) + if use_fp4_cache: + if T_new >= 2: + self._nvfp4_stored_fp4 = new_fp4.view(B, T_new, H, W, Ci // 2)[:, -_CACHE_T:].contiguous().view(-1).clone() + self._nvfp4_stored_sf = new_sf.view(B, T_new, H, W, Ci // 16)[:, -_CACHE_T:].contiguous().view(-1).clone() + self._nvfp4_stored_T = _CACHE_T + elif T_new == 1 and stored_fp4 is not None and stored_T >= 1: + # Rolling: [prev_last_frame, current_frame] + prev_fp4_5d = stored_fp4.view(B, stored_T, H, W, Ci // 2)[:, -1:] + curr_fp4_5d = new_fp4.view(B, 1, H, W, Ci // 2) + self._nvfp4_stored_fp4 = torch.cat([prev_fp4_5d, curr_fp4_5d], dim=1).contiguous().view(-1).clone() + prev_sf_5d = stored_sf.view(B, stored_T, H, W, Ci // 16)[:, -1:] + curr_sf_5d = new_sf.view(B, 1, H, W, Ci // 16) + self._nvfp4_stored_sf = torch.cat([prev_sf_5d, curr_sf_5d], dim=1).contiguous().view(-1).clone() + self._nvfp4_stored_T = _CACHE_T + else: + self._nvfp4_stored_fp4 = new_fp4.clone() + self._nvfp4_stored_sf = new_sf.clone() + self._nvfp4_stored_T = T_new + + out = out_ndhwc.view(B, T_new, H, W, Co).permute(0, 4, 1, 2, 3) + return out.contiguous(memory_format=torch.channels_last_3d) + + +# ════════════════════════════════════════════════════════════════════ +# Direction 2+3: Fused norm+silu+quant + FP4 cache +# Eliminates both activation quant and cache quant per call. +# ════════════════════════════════════════════════════════════════════ + +_CACHE_T = 2 + +def _fused_norm_silu_quant_cl(x, gamma, bias, B, C, T, H, W): + """Call fused RMS+SiLU+NVFP4quant kernel (channels-last input). + Returns (fp4_flat, sf_flat). + """ + n = B * T * H * W + fp4 = torch.empty(n * (C // 2), dtype=torch.uint8, device=x.device) + sf = torch.empty(n * (C // 16), dtype=torch.uint8, device=x.device) + s = torch.cuda.current_stream().cuda_stream + # Ensure channels-last contiguous + if not x.is_contiguous(memory_format=torch.channels_last_3d): + x = x.to(memory_format=torch.channels_last_3d) + bias_ptr = bias.data_ptr() if isinstance(bias, torch.Tensor) else 0 + rc = _fvk.fp16_rms_silu_quant_nvfp4_cl_ndhwc( + x.data_ptr(), gamma.data_ptr(), bias_ptr, + fp4.data_ptr(), sf.data_ptr(), + B, C, T, H, W, 1e-6, s) + if rc != 0: + raise RuntimeError(f"[nvfp4_vae] fused_norm_silu_quant rc={rc}") + return fp4, sf + + +def _conv3d_prequant(conv, new_fp4, new_sf, cache_fp4, cache_sf, + B, Ci, Co, T_new, H, W): + """Conv3d with pre-quantized FP4 data (no activation/cache quant needed).""" + s = torch.cuda.current_stream().cuda_stream + M_total = B * T_new * H * W + out_ndhwc = torch.empty(M_total * Co, dtype=_FP16, device=conv._nvfp4_w.device) + bias_ptr = conv.bias.data_ptr() if conv.bias is not None else 0 + rc = _fvk.nvfp4_conv3d_ndhwc_fp16out( + cache_fp4.data_ptr(), new_fp4.data_ptr(), + conv._nvfp4_w.data_ptr(), + cache_sf.data_ptr(), new_sf.data_ptr(), + conv._nvfp4_sf.data_ptr(), + out_ndhwc.data_ptr(), bias_ptr, + B, _CACHE_T, T_new, H, W, Ci, Co, 1.0, s) + if rc != 0: + raise RuntimeError(f"[nvfp4_vae] conv3d_prequant rc={rc}") + out = out_ndhwc.view(B, T_new, H, W, Co).permute(0, 4, 1, 2, 3) + return out.contiguous(memory_format=torch.channels_last_3d) + + +def _slice_fp4_cache(fp4_flat, sf_flat, B, C, T, H, W): + """Extract last 2 frames from flat FP4+SF as cache.""" + fp4_5d = fp4_flat.view(B, T, H, W, C // 2) + sf_5d = sf_flat.view(B, T, H, W, C // 16) + return (fp4_5d[:, -_CACHE_T:].contiguous().view(-1), + sf_5d[:, -_CACHE_T:].contiguous().view(-1)) + + +def _build_fp4_cache(conv, B, C, H, W, device): + """Construct 2-frame FP4 cache from previous call's stored output. + Handles T_prev < 2 by zero-padding (matches FP8 path behavior).""" + prev_fp4 = getattr(conv, '_nvfp4_cache_fp4', None) + prev_sf = getattr(conv, '_nvfp4_cache_sf', None) + prev_T = getattr(conv, '_nvfp4_cache_T', 0) + + if prev_fp4 is not None and prev_T >= 2: + fp4_5d = prev_fp4.view(B, prev_T, H, W, C // 2) + sf_5d = prev_sf.view(B, prev_T, H, W, C // 16) + return (fp4_5d[:, -_CACHE_T:].contiguous().view(-1), + sf_5d[:, -_CACHE_T:].contiguous().view(-1)) + elif prev_fp4 is not None and prev_T == 1: + # Pad: [zero_frame, prev_frame] (prev at index 1) + cache_fp4 = torch.zeros(B * _CACHE_T * H * W * (C // 2), + dtype=torch.uint8, device=device) + cache_sf = torch.zeros(B * _CACHE_T * H * W * (C // 16), + dtype=torch.uint8, device=device) + off = B * H * W * (C // 2) # offset to second frame + cache_fp4[off:off + prev_fp4.numel()] = prev_fp4 + off_sf = B * H * W * (C // 16) + cache_sf[off_sf:off_sf + prev_sf.numel()] = prev_sf + return cache_fp4, cache_sf + else: + return (torch.zeros(B * _CACHE_T * H * W * (C // 2), + dtype=torch.uint8, device=device), + torch.zeros(B * _CACHE_T * H * W * (C // 16), + dtype=torch.uint8, device=device)) + + +def _make_patched_residual_forward(orig_forward): + """Create a patched WanResidualBlock.forward that fuses norm+quant + and uses FP4 cache for blocks where both conv1 and conv2 are NVFP4-eligible. + """ + + def _patched_forward(self, x, feat_cache=None, feat_idx=[0]): + # Only handle blocks where BOTH convs are NVFP4-eligible. + conv1_nvfp4 = getattr(self.conv1, '_nvfp4_w', None) is not None + conv2_nvfp4 = getattr(self.conv2, '_nvfp4_w', None) is not None + if not (conv1_nvfp4 and conv2_nvfp4): + return orig_forward(self, x, feat_cache, feat_idx) + + try: + from diffusers.models.autoencoders.autoencoder_kl_wan import CACHE_T + except Exception: + CACHE_T = 2 + stream = torch.cuda.current_stream().cuda_stream + + def _to_cl(t): + if not t.is_contiguous(memory_format=torch.channels_last_3d): + return t.to(memory_format=torch.channels_last_3d) + return t + + # Shortcut (fp16, unchanged) -- conv_shortcut may itself be NVFP4/FP8. + h = self.conv_shortcut(x) + + def _norm_quant(norm, xin): + """Fused RMS+SiLU+NVFP4-quant -> (new_fp4 flat, new_sf flat).""" + B, C, T, Hh, Ww = xin.shape + gamma = norm.gamma + bias = getattr(norm, 'bias', 0) + bias_ptr = bias.data_ptr() if isinstance(bias, torch.Tensor) else 0 + gamma_flat = gamma.contiguous().view(-1).to(_FP16) if gamma.dtype != _FP16 else gamma.contiguous().view(-1) + n = B * T * Hh * Ww + fp4 = torch.empty(n * (C // 2), dtype=torch.uint8, device=xin.device) + sf = torch.empty(n * (C // 16), dtype=torch.uint8, device=xin.device) + rc = _fvk.fp16_rms_silu_quant_nvfp4_cl_ndhwc( + xin.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + fp4.data_ptr(), sf.data_ptr(), + B, C, T, Hh, Ww, 1e-6, stream) + if rc != 0: + raise RuntimeError(f"[nvfp4_fused] norm_quant rc={rc}") + return fp4, sf + + def _conv_prequant(conv, new_fp4, new_sf, B, Ci, T_new, Hh, Ww, device): + """NVFP4 conv with pre-quantized new activation + rolling FP4 cache + reuse (mirrors Direction-2's _nvfp4_conv3d_forward cache logic, but + skips the per-call new-x quant since new_fp4 comes fused from the + norm). This keeps the cache-reuse win AND the norm+quant fusion.""" + Co = conv._nvfp4_w.shape[0] + use_fp4_cache = os.environ.get('FLASHRT_NVFP4_NO_CACHE', '0') != '1' + stored_fp4 = getattr(conv, '_nvfp4_stored_fp4', None) if use_fp4_cache else None + stored_sf = getattr(conv, '_nvfp4_stored_sf', None) if use_fp4_cache else None + stored_T = getattr(conv, '_nvfp4_stored_T', 0) if use_fp4_cache else 0 + + if stored_fp4 is not None and stored_T >= 2: + cache_fp4 = stored_fp4; cache_sf = stored_sf + elif stored_fp4 is not None and stored_T == 1: + # Previous call had T_new=1: pad [zero, prev_frame] (Direction-2) + cache_fp4 = torch.zeros(B * _CACHE_T * Hh * Ww * (Ci // 2), + dtype=torch.uint8, device=device) + cache_sf = torch.zeros(B * _CACHE_T * Hh * Ww * (Ci // 16), + dtype=torch.uint8, device=device) + off = B * Hh * Ww * (Ci // 2) + cache_fp4[off:off + stored_fp4.numel()] = stored_fp4 + off_sf = B * Hh * Ww * (Ci // 16) + cache_sf[off_sf:off_sf + stored_sf.numel()] = stored_sf + else: + cache_fp4 = torch.zeros(B * _CACHE_T * Hh * Ww * (Ci // 2), + dtype=torch.uint8, device=device) + cache_sf = torch.zeros(B * _CACHE_T * Hh * Ww * (Ci // 16), + dtype=torch.uint8, device=device) + + M_total = B * T_new * Hh * Ww + out_ndhwc = torch.empty(M_total * Co, dtype=_FP16, device=device) + bias_ptr = conv.bias.data_ptr() if conv.bias is not None else 0 + rc = _fvk.nvfp4_conv3d_ndhwc_fp16out( + cache_fp4.data_ptr(), new_fp4.data_ptr(), + conv._nvfp4_w.data_ptr(), + cache_sf.data_ptr(), new_sf.data_ptr(), + conv._nvfp4_sf.data_ptr(), + out_ndhwc.data_ptr(), bias_ptr, + B, _CACHE_T, T_new, Hh, Ww, Ci, Co, 1.0, stream) + if rc != 0: + raise RuntimeError(f"[nvfp4_fused] conv3d_prequant rc={rc}") + + # Store rolling 2-frame FP4 cache for next call (Direction-2 logic). + if use_fp4_cache: + if T_new >= 2: + conv._nvfp4_stored_fp4 = new_fp4.view(B, T_new, Hh, Ww, Ci // 2)[:, -_CACHE_T:].contiguous().view(-1).clone() + conv._nvfp4_stored_sf = new_sf.view(B, T_new, Hh, Ww, Ci // 16)[:, -_CACHE_T:].contiguous().view(-1).clone() + conv._nvfp4_stored_T = _CACHE_T + elif T_new == 1 and stored_fp4 is not None and stored_T >= 1: + prev_fp4 = stored_fp4.view(B, stored_T, Hh, Ww, Ci // 2)[:, -1:] + curr_fp4 = new_fp4.view(B, 1, Hh, Ww, Ci // 2) + conv._nvfp4_stored_fp4 = torch.cat([prev_fp4, curr_fp4], dim=1).contiguous().view(-1).clone() + prev_sf = stored_sf.view(B, stored_T, Hh, Ww, Ci // 16)[:, -1:] + curr_sf = new_sf.view(B, 1, Hh, Ww, Ci // 16) + conv._nvfp4_stored_sf = torch.cat([prev_sf, curr_sf], dim=1).contiguous().view(-1).clone() + conv._nvfp4_stored_T = _CACHE_T + else: + conv._nvfp4_stored_fp4 = new_fp4.clone() + conv._nvfp4_stored_sf = new_sf.clone() + conv._nvfp4_stored_T = T_new + + out = out_ndhwc.view(B, T_new, Hh, Ww, Co).permute(0, 4, 1, 2, 3) + return out.contiguous(memory_format=torch.channels_last_3d) + + # ── norm1 + conv1 ────────────────────────────────────────── + x_cl = _to_cl(x) + B0, C0, T0, H0, W0 = x_cl.shape + new_fp4_1, new_sf_1 = _norm_quant(self.norm1, x_cl) + x_out = _conv_prequant(self.conv1, new_fp4_1, new_sf_1, + B0, C0, T0, H0, W0, x_cl.device) + if feat_cache is not None: + feat_idx[0] += 1 + + # ── norm2 + conv2 ────────────────────────────────────────── + x_out_cl = _to_cl(x_out) + B1, C1, T1, H1, W1 = x_out_cl.shape + new_fp4_2, new_sf_2 = _norm_quant(self.norm2, x_out_cl) + x_out2 = _conv_prequant(self.conv2, new_fp4_2, new_sf_2, + B1, C1, T1, H1, W1, x_out_cl.device) + if feat_cache is not None: + feat_idx[0] += 1 + + return x_out2 + h + + return _patched_forward + + +# ── Install ── + +def install_vae_nvfp4(vae, enabled: bool = True) -> Dict: + """Install NVFP4 W4A4 conv3d for eligible WanVAE layers. + + Must be called AFTER ``install_vae_optimizations`` (the FP8 path + must already be installed — FP4 overrides eligible layers). + + Returns summary dict. + """ + if not enabled: + return {'enabled': False, 'reason': 'disabled'} + + if os.environ.get('FLASHRT_NVFP4_VAE', '1') != '1': + return {'enabled': False, 'reason': 'env disabled'} + + if not _load_kernels(): + return {'enabled': False, 'reason': 'kernels not found'} + + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanCausalConv3d) + + decode_only = os.environ.get('FLASHRT_NVFP4_VAE_DECODE_ONLY', '0') == '1' + min_ci = int(os.environ.get('FLASHRT_NVFP4_VAE_MIN_CI', '192')) + + n_quantized = 0 + n_skipped = 0 + skip_reasons: Dict[str, int] = {} + + for name, m in vae.named_modules(): + if not isinstance(m, WanCausalConv3d): + continue + kt, kh, kw = m.kernel_size + Ci, Co = m.in_channels, m.out_channels + if (kt, kh, kw) != (3, 3, 3): + continue + if Ci % 96 != 0 or Ci < min_ci or Ci % 64 != 0: + n_skipped += 1 + r = f'Ci={Ci} (<{min_ci} or not %64 for NVFP4 MMA)' + skip_reasons[r] = skip_reasons.get(r, 0) + 1 + continue + if decode_only and name.startswith('encoder.'): + n_skipped += 1 + r = 'encoder (decode_only)' + skip_reasons[r] = skip_reasons.get(r, 0) + 1 + continue + + # Pre-quantize weight + w_fp4, w_sf = _quantize_weight_nvfp4(m.weight.data) + m.register_buffer('_nvfp4_w', w_fp4, persistent=False) + m.register_buffer('_nvfp4_sf', w_sf, persistent=False) + n_quantized += 1 + + if n_quantized == 0: + logger.warning("[nvfp4_vae] 0 layers eligible for FP4") + return {'enabled': False, 'n_quantized': 0, 'n_skipped': n_skipped, + 'skip_reasons': skip_reasons} + + # Patch forward — override FP8 dispatch for FP4-eligible layers + _saved_forward = WanCausalConv3d.forward + + def _nvfp4_dispatch_forward(self, x, cache_x=None): + if getattr(self, '_nvfp4_w', None) is not None: + return _nvfp4_conv3d_forward(self, x, cache_x) + return _saved_forward(self, x, cache_x) + + WanCausalConv3d.forward = _nvfp4_dispatch_forward + WanCausalConv3d._flashrt_nvfp4_patched = True + + # ── Direction 2+3: Patch WanResidualBlock for fused norm+quant + FP4 cache ── + n_fused_blocks = 0 + # Direction 3 ON by default: fused norm+silu+NVFP4-quant (kernel verified + # byte-correct 99.7%) + streaming-correct cache (mirrors diffusers' + # [<2 frames] padding for the per-frame decode loop). End-to-end PSNR + # 35.23 dB vs 35.11 baseline. Disable with FLASHRT_NVFP4_FUSED_NORMQUANT=0. + no_fused = os.environ.get('FLASHRT_NVFP4_FUSED_NORMQUANT', '1') == '0' + try: + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanResidualBlock) + _orig_res_forward = WanResidualBlock.forward + _patched_fwd = _make_patched_residual_forward(_orig_res_forward) + + # Check which blocks have both conv1+conv2 NVFP4-eligible + for blk in vae.modules(): + if isinstance(blk, WanResidualBlock): + c1_ok = getattr(blk.conv1, '_nvfp4_w', None) is not None + c2_ok = getattr(blk.conv2, '_nvfp4_w', None) is not None + if c1_ok and c2_ok: + blk._nvfp4_fused = True + n_fused_blocks += 1 + + if n_fused_blocks > 0 and not no_fused: + WanResidualBlock.forward = _patched_fwd + logger.info("[nvfp4_vae] %d WanResidualBlocks use fused " + "norm+quant + FP4 cache", n_fused_blocks) + + # Patch clear_cache to also clear FP4 caches between encode/decode passes + import types + _orig_clear = vae.clear_cache + def _clear_with_fp4(self_vae): + _orig_clear() + for m in self_vae.modules(): + m._nvfp4_stored_fp4 = None + m._nvfp4_stored_sf = None + m._nvfp4_stored_T = 0 + vae.clear_cache = types.MethodType(_clear_with_fp4, vae) + except Exception as e: + logger.debug("[nvfp4_vae] fused block patch skipped: %s", e) + + summary = { + 'enabled': True, + 'n_quantized': n_quantized, + 'n_skipped': n_skipped, + 'n_fused_blocks': n_fused_blocks, + 'skip_reasons': skip_reasons, + 'decode_only': decode_only, + 'min_ci': min_ci, + } + logger.info("[nvfp4_vae] install summary: %s", summary) + return summary diff --git a/flash_rt/models/minimax_remover/_vae_opt.py b/flash_rt/models/minimax_remover/_vae_opt.py new file mode 100644 index 00000000..cf08854e --- /dev/null +++ b/flash_rt/models/minimax_remover/_vae_opt.py @@ -0,0 +1,880 @@ +"""FlashRT -- MiniMax-Remover VAE optimization: fp16-native fused kernels. + +Replaces diffusers WanRMS_norm.forward (4 full-tensor fp32 passes, +~0.45 ms each at [1,384,1,240,432]) with the FlashRT +``fp16_rms_norm_ncdhw`` CUDA kernel (single-pass, fp16 in/out, fp32 +internal statistics, ~0.07 ms -- a ~6x speed-up per call). + +Additionally fuses RMS_norm + SiLU in every WanResidualBlock via +``fp16_rms_silu_ncdhw`` (one pass instead of norm->write->silu->write), +and eliminates the redundant fp32 cast in WanUpsample (nearest-exact +upsample is index-only, so fp16 == fp32 bit-for-bit). + +Key design decision: **no dtype cast**. The VAE stays in fp16 (cuDNN +already dispatches fp16 tensorop conv kernels). Only the norm/activation +ops are replaced. This preserves fp16's 10-bit mantissa end-to-end, +keeping PSNR at ~40 dB vs the fp16 reference (vs ~15 dB for the +bf16-cast path which loses 3 bits of mantissa across 52 RMS_norm +layers). +""" +from __future__ import annotations + +import logging +import os +from typing import Dict + +import torch +import torch.nn as nn +import torch.nn.functional as F + +logger = logging.getLogger(__name__) + +# The fp16 fused kernels live in a standalone pybind module +# (flash_rt_minimax_remover) that is opt-in: +# cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ... +_fvk = None +try: + from flash_rt import flash_rt_minimax_remover as _fvk +except ImportError: + try: + import flash_rt_minimax_remover as _fvk + except ImportError: + pass + +_FP16 = torch.float16 +_EPS = 1e-6 + + +def _shape_ncdhw(x: torch.Tensor): + """Return (B, C, T, H, W) for 4D/5D NCDHW tensors, else None.""" + if x.dim() == 4: + B, C, H, W = x.shape + return B, C, 1, H, W + if x.dim() == 5: + B, C, T, H, W = x.shape + return B, C, T, H, W + return None + + +def _prep_gamma_bias(gamma, bias): + """Return contiguous fp16 (gamma_flat, bias_ptr) for the kernels.""" + if gamma.dtype != _FP16: + gamma = gamma.to(_FP16) + gamma_flat = gamma.contiguous().view(-1) + if isinstance(bias, torch.Tensor): + bias_flat = bias.contiguous().view(-1).to(_FP16) + return gamma_flat, bias_flat.data_ptr() + return gamma_flat, 0 + + +def _ref_rms_norm(gamma, bias, x): + """Reference fallback (fp32 stats, fp16 out) -- WanRMS_norm semantics.""" + C = x.shape[1] + scale = C ** 0.5 + out = torch.nn.functional.normalize(x.float(), dim=1).to(x.dtype) + return out * scale * gamma + (bias if isinstance(bias, torch.Tensor) else 0.0) + + +def _flashrt_fp16_rms_norm_forward(self, x: torch.Tensor) -> torch.Tensor: + """FlashRT fp16-native RMS_norm replacement for WanRMS_norm.forward. + + Computes: y = (x / rms(x)) * gamma + bias (fp16 in/out, fp32 stats) + which equals WanRMS_norm's F.normalize(x, dim=1) * sqrt(C) * gamma. + """ + shp = _shape_ncdhw(x) + if shp is None: + return self._orig_forward(x) + B, C, T, H, W = shp + + if x.dtype != _FP16: + x = x.to(_FP16) + if not x.is_contiguous(): + x = x.contiguous() + + gamma_flat, bias_ptr = _prep_gamma_bias(self.gamma, self.bias) + out = torch.empty_like(x) + stream = torch.cuda.current_stream().cuda_stream + rc = _fvk.fp16_rms_norm_ncdhw( + x.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), B, C, T, H, W, _EPS, stream) + if rc != 0: + return _ref_rms_norm(self.gamma, self.bias, x) + return out + + +class _FusedRmsSilu(nn.Module): + """Drop-in for WanRMS_norm that outputs silu(rms_norm(x)) in one kernel. + + Installed as WanResidualBlock.norm1/norm2 while the block's + ``nonlinearity`` is swapped to ``Identity`` -- so the existing + ``forward`` (norm1 -> nonlinearity -> conv1 -> norm2 -> nonlinearity + -> conv2) silently becomes a fused norm+silu path with no rewrite of + the complex causal-cache logic. + """ + + def __init__(self, gamma, bias): + super().__init__() + self.gamma = gamma + self.bias = bias + + def forward(self, x: torch.Tensor) -> torch.Tensor: + shp = _shape_ncdhw(x) + if shp is None: + return torch.nn.functional.silu(_ref_rms_norm(self.gamma, self.bias, x)) + B, C, T, H, W = shp + + if x.dtype != _FP16: + x = x.to(_FP16) + if not x.is_contiguous(): + x = x.contiguous() + + gamma_flat, bias_ptr = _prep_gamma_bias(self.gamma, self.bias) + out = torch.empty_like(x) + stream = torch.cuda.current_stream().cuda_stream + rc = _fvk.fp16_rms_silu_ncdhw( + x.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), B, C, T, H, W, _EPS, stream) + if rc != 0: + return torch.nn.functional.silu(_ref_rms_norm(self.gamma, self.bias, x)) + return out + + +def install_flashrt_fp16_rms_norm(vae) -> int: + """Replace WanRMS_norm.forward (attention sites) with the FlashRT fp16 + kernel, and fuse norm+silu inside every WanResidualBlock. + + Attention-block norms (WanAttentionBlock) keep the plain rms_norm + kernel (no SiLU follows them). Residual-block norms (norm1/norm2) + are swapped to the fused ``fp16_rms_silu_ncdhw`` kernel and the + block's SiLU is set to Identity, so the existing ``forward`` runs the + fused path without touching the causal-cache logic. + + Returns the count of patched modules. + """ + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanRMS_norm, WanResidualBlock) + + n_fused = 0 + for blk in vae.modules(): + if isinstance(blk, WanResidualBlock): + blk.norm1 = _FusedRmsSilu(blk.norm1.gamma, blk.norm1.bias) + blk.norm2 = _FusedRmsSilu(blk.norm2.gamma, blk.norm2.bias) + blk.nonlinearity = nn.Identity() + n_fused += 1 + + if not getattr(WanRMS_norm, "_flashrt_fp16_patched", False): + WanRMS_norm._orig_forward = WanRMS_norm.forward + WanRMS_norm.forward = _flashrt_fp16_rms_norm_forward + WanRMS_norm._flashrt_fp16_patched = True + logger.info("[minimax-vae] patched WanRMS_norm.forward -> FlashRT " + "fp16_rms_norm_ncdhw (fp16-native, no cast, ~6x faster)") + + logger.info("[minimax-vae] %d WanResidualBlock(s) now use fused " + "fp16_rms_silu_ncdhw (norm+silu in one pass)", n_fused) + return n_fused + + +def _install_wan_upsample_no_cast(vae) -> int: + """Eliminate the redundant fp32 cast in WanUpsample. + + WanUpsample.forward does ``super().forward(x.float()).type_as(x)``. + For ``nearest-exact`` mode the upsample is pure index selection (no + arithmetic), so fp16 and fp32 give bit-identical results -- the cast + is wasted bandwidth. This swaps it to a fp16-native forward. + """ + from diffusers.models.autoencoders.autoencoder_kl_wan import WanUpsample + + if not getattr(WanUpsample, "_flashrt_nocast", False): + _orig_upsample_forward = WanUpsample.forward + + def _no_cast_forward(self, x): + if self.mode == "nearest-exact": + return nn.Upsample.forward(self, x) + return _orig_upsample_forward(self, x) + + WanUpsample.forward = _no_cast_forward + WanUpsample._flashrt_nocast = True + logger.info("[minimax-vae] patched WanUpsample.forward -> " + "fp16-native (nearest-exact cast eliminated)") + + return sum(1 for m in vae.modules() if isinstance(m, WanUpsample)) + + +def install_vae_optimizations(vae, dtype=None, use_fp8_conv: bool = True) -> Dict: + """Apply VAE optimizations: fp16-native fused RMS_norm + RMS_SiLU kernels + + WanUpsample cast elimination + channels-last 3D pipeline + FP8 + implicit-GEMM conv3d. + + No dtype cast is applied -- the VAE stays fp16. Only norm/activation + ops are replaced with FlashRT fp16 CUDA kernels, and applicable 3x3x3 + conv3d layers are replaced with FP8 implicit-GEMM kernels (fp16 in/out, + FP8 e4m3 internal compute). + + Requires the standalone ``flash_rt_minimax_remover`` module, built with: + cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ... + + Args: + vae: loaded ``diffusers.AutoencoderKLWan`` instance. + dtype: ignored (kept for API compat with the old bf16 interface). + use_fp8_conv: if True, replace applicable 3x3x3 conv3d with the + FP8 implicit-GEMM kernel (default True). + + Returns: + stats dict. + + Raises: + ImportError if flash_rt_minimax_remover is not built. + """ + if _fvk is None: + raise ImportError( + "flash_rt_minimax_remover not found; rebuild FlashRT with: " + "cmake -DFLASHRT_ENABLE_MINIMAX_REMOVER=ON -DGPU_ARCH=120 ...") + n_fused_blocks = install_flashrt_fp16_rms_norm(vae) + n_upsample = _install_wan_upsample_no_cast(vae) + _install_channels_last_pipeline(vae) + n_fp8_conv = _install_fp8_conv3d_pipeline(vae, enabled=use_fp8_conv) + return { + "n_fused_res_blocks": n_fused_blocks, + "n_upsample_nocast": n_upsample, + "n_fp8_conv3d": n_fp8_conv, + "vae_dtype": str(next(vae.parameters()).dtype), + } + + +def _install_channels_last_pipeline(vae) -> int: + """Enable channels-last 3D (NDHWC) throughout the VAE pipeline. + + Converts Conv3d weights to channels-last, patches WanCausalConv3d + to preserve the format, and replaces the FlashRT norm kernels with + channels-last variants so norm output stays NDHWC. + + This eliminates ALL nchw↔nhwc conversion kernels that cuDNN inserts + (~287 ms / decode) and gives a ~1.3x speedup on the dominant conv + layers. Zero precision loss (identical fp16 computation, only the + memory layout changes). + """ + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanCausalConv3d, WanRMS_norm) + + n = 0 + + # 1. Convert Conv3d weights to channels-last 3D. + for m in vae.modules(): + if isinstance(m, WanCausalConv3d): + with torch.no_grad(): + m.weight.data = m.weight.data.to( + memory_format=torch.channels_last_3d) + n += 1 + + # 2. Patch WanCausalConv3d.forward to preserve channels-last. + if not getattr(WanCausalConv3d, "_flashrt_cl_fwd", False): + def _cl_conv_forward(self, x, cache_x=None): + if x.dim() == 5 and not x.is_contiguous( + memory_format=torch.channels_last_3d): + x = x.to(memory_format=torch.channels_last_3d) + padding = list(self._padding) + if cache_x is not None and self._padding[4] > 0: + cache_x = cache_x.to(x.device) + if not cache_x.is_contiguous( + memory_format=torch.channels_last_3d): + cache_x = cache_x.to( + memory_format=torch.channels_last_3d) + x = torch.cat([cache_x, x], dim=2) + padding[4] -= cache_x.shape[2] + x = F.pad(x, padding) + return F.conv3d(x, self.weight, self.bias, self.stride, + self.padding, self.dilation, self.groups) + + WanCausalConv3d.forward = _cl_conv_forward + WanCausalConv3d._flashrt_cl_fwd = True + + # 3. Replace norm kernels with channels-last variants. + def _cl_rms_norm_forward(self, x): + if x.dim() == 4: + # Attention block reshapes to 4D [B*T, C, H, W] — use NCDHW + # kernel (attention is ~2% of decode time, conversion negligible). + return _flashrt_fp16_rms_norm_forward(self, x) + B, C, T, H, W = x.shape + if x.dtype != _FP16: + x = x.to(_FP16) + if not x.is_contiguous(memory_format=torch.channels_last_3d): + x = x.to(memory_format=torch.channels_last_3d) + gamma_flat, bias_ptr = _prep_gamma_bias(self.gamma, self.bias) + out = torch.empty_like(x) + stream = torch.cuda.current_stream().cuda_stream + rc = _fvk.fp16_rms_norm_ndhwc( + x.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), B, C, T, H, W, _EPS, stream) + if rc != 0: + return _ref_rms_norm(self.gamma, self.bias, x) + return out + + class _FusedRmsSiluCL(nn.Module): + """Fused RMSNorm+SiLU with optional amax computation for FP8 conv. + + When _fp8_sister_conv is set (by the FP8 conv pipeline), the + forward computes the output amax via the fused + fp16_rms_silu_amax_ndhwc kernel and accumulates it into a + shared running-amax buffer via atomicMax. + + The running-max buffer is shared with the sister conv so the + conv can skip its own amax pass over both x AND cache_x (the + cache was a previous output of this same norm, so its amax + was already accumulated in a prior iteration). + """ + def __init__(self, gamma, bias): + super().__init__() + self.gamma = gamma + self.bias = bias + self._fp8_sister_conv = None + self._amax_buf = None + self._running_mode = False + + def _ensure_amax_buf(self, device): + if self._amax_buf is None or self._amax_buf.device != device: + self._amax_buf = torch.zeros( + 1, dtype=torch.float32, device=device) + + def forward(self, x): + if x.dim() == 4: + return F.silu(_flashrt_fp16_rms_norm_forward( + type('_DummyNorm', (), { + 'gamma': self.gamma, 'bias': self.bias, + '_orig_forward': None})(), x)) + B, C, T, H, W = x.shape + if x.dtype != _FP16: + x = x.to(_FP16) + if not x.is_contiguous(memory_format=torch.channels_last_3d): + x = x.to(memory_format=torch.channels_last_3d) + gamma_flat, bias_ptr = _prep_gamma_bias(self.gamma, self.bias) + out = torch.empty_like(x) + stream = torch.cuda.current_stream().cuda_stream + + if self._fp8_sister_conv is not None: + self._ensure_amax_buf(x.device) + if not self._running_mode: + self._amax_buf.zero_() + rc = _fvk.fp16_rms_silu_amax_ndhwc( + x.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), self._amax_buf.data_ptr(), + B, C, T, H, W, _EPS, stream) + else: + rc = _fvk.fp16_rms_silu_ndhwc( + x.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), B, C, T, H, W, _EPS, stream) + + if rc != 0: + return F.silu(_ref_rms_norm(self.gamma, self.bias, x)) + return out + + # Swap residual-block norms to CL fused variant. + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanResidualBlock) + for blk in vae.modules(): + if isinstance(blk, WanResidualBlock): + blk.norm1 = _FusedRmsSiluCL(blk.norm1.gamma, blk.norm1.bias) + blk.norm2 = _FusedRmsSiluCL(blk.norm2.gamma, blk.norm2.bias) + + # Swap attention-block norm to CL plain variant. + WanRMS_norm.forward = _cl_rms_norm_forward + WanRMS_norm._flashrt_cl_norm = True + + logger.info("[minimax-vae] channels-last 3D pipeline enabled: " + "%d Conv3d weights converted, norm kernels swapped to " + "NDHWC variant (eliminates ~287ms format conversion)", + n) + return n + + +# ================================================================ +# FP8 implicit-GEMM conv3d pipeline +# ================================================================ + +_FP8_E4M3_MAX = 448.0 + + +def _prequantize_conv3d_weight(conv): + """Pre-quantize a WanCausalConv3d 3x3x3 weight to FP8 e4m3. + + Returns (w_fp8 [Co,3,3,3,Ci] fp8, w_scale [Co] float32) or + (None, None) if the conv is not applicable. + """ + kt, kh, kw = conv.kernel_size + Ci, Co = conv.in_channels, conv.out_channels + if ((kt, kh, kw) != (3, 3, 3) or Ci % 32 != 0 or Co < 8 or + conv.groups != 1): + return None, None + + w = conv.weight.data + if not w.is_contiguous(memory_format=torch.channels_last_3d): + w = w.to(memory_format=torch.channels_last_3d) + w_perm = w.permute(0, 2, 3, 4, 1).contiguous().float() + + w_scale = w_perm.reshape(Co, -1).abs().amax(dim=1) / _FP8_E4M3_MAX + w_scale = w_scale.clamp(min=1e-6) + + w_scaled = w_perm / w_scale.reshape(-1, 1, 1, 1, 1) + w_fp8 = w_scaled.clamp(-_FP8_E4M3_MAX, _FP8_E4M3_MAX).to( + torch.float8_e4m3fn) + return w_fp8.contiguous(), w_scale + + +def _fp8_conv3d_forward(self, x, cache_x=None): + """FP8 implicit-GEMM conv3d forward for 3x3x3 causal convs. + + Uses a running-max amax strategy: the sister-norm module + accumulates the output amax of x via atomicMax into a shared + buffer across iterations. Since cache_x was a previous output + of the SAME norm module, its amax is already covered by the + running max — so we skip the cache amax pass entirely. + + This saves one full read of the cache tensor per layer (~40 MB + for the largest layers). + """ + B, Ci, T_new, H, W = x.shape + Co = self._fp8_w.shape[0] + stream = torch.cuda.current_stream().cuda_stream + + if not x.is_contiguous(memory_format=torch.channels_last_3d): + x = x.to(memory_format=torch.channels_last_3d) + + # ── Resolve cache ──────────────────────────────────────────── + if cache_x is not None and cache_x.shape[2] >= 2: + cache_2 = cache_x[:, :, -2:] + if not cache_2.is_contiguous(memory_format=torch.channels_last_3d): + cache_2 = cache_2.to(memory_format=torch.channels_last_3d) + elif cache_x is not None and cache_x.shape[2] >= 1: + cache_2 = torch.empty( + B, Ci, 2, H, W, dtype=x.dtype, device=x.device, + memory_format=torch.channels_last_3d).zero_() + cache_2[:, :, 1:2] = cache_x[:, :, -1:] + else: + cache_2 = torch.empty( + B, Ci, 2, H, W, dtype=x.dtype, device=x.device, + memory_format=torch.channels_last_3d).zero_() + + n_cache = cache_2.numel() + n_new = x.numel() + + # ── Determine amax ─────────────────────────────────────────── + sister_norm = getattr(self, '_sister_norm', None) + sister_amax = getattr(sister_norm, '_amax_buf', None) if sister_norm else None + running_mode = getattr(sister_norm, '_running_mode', False) if sister_norm else False + + if sister_amax is not None and sister_amax.is_cuda and running_mode: + # Running-max path: amax of x already accumulated by norm. + # cache_x amax was accumulated in a previous iteration (same norm). + # No cache amax call needed! + shared_amax = sister_amax + elif sister_amax is not None and sister_amax.is_cuda: + # First-iteration path: norm computed x's amax, accumulate cache amax. + shared_amax = sister_amax + _fvk.amax_fp16(cache_2.data_ptr(), shared_amax.data_ptr(), + n_cache, stream) + else: + # Fallback: compute amax from scratch. + shared_amax = self._fp8_amax + shared_amax.zero_() + _fvk.amax_fp16(cache_2.data_ptr(), shared_amax.data_ptr(), + n_cache, stream) + _fvk.amax_fp16(x.data_ptr(), shared_amax.data_ptr(), + n_new, stream) + + # ── Quantize cache + new with shared amax (single launch) ── + cache_fp8 = torch.empty( + n_cache, dtype=torch.float8_e4m3fn, device=x.device) + new_fp8 = torch.empty( + n_new, dtype=torch.float8_e4m3fn, device=x.device) + if hasattr(_fvk, 'quantize_fp16_fp8_with_amax_dual'): + _fvk.quantize_fp16_fp8_with_amax_dual( + cache_2.data_ptr(), cache_fp8.data_ptr(), n_cache, + x.data_ptr(), new_fp8.data_ptr(), n_new, + shared_amax.data_ptr(), self._fp8_scale.data_ptr(), + stream) + else: + _fvk.quantize_fp16_fp8_with_amax( + cache_2.data_ptr(), cache_fp8.data_ptr(), + shared_amax.data_ptr(), self._fp8_scale.data_ptr(), + n_cache, stream) + _fvk.quantize_fp16_fp8_with_amax( + x.data_ptr(), new_fp8.data_ptr(), + shared_amax.data_ptr(), self._fp8_scale.data_ptr(), + n_new, stream) + + alpha_vec = self._fp8_scale * self._w_scale + + out = torch.empty( + B, Co, T_new, H, W, dtype=torch.float16, device=x.device, + memory_format=torch.channels_last_3d) + + rc = _fvk.fp8_conv3d_mm_ndhwc_fp16out( + cache_fp8.data_ptr(), new_fp8.data_ptr(), + self._fp8_w.data_ptr(), out.data_ptr(), + self.bias.data_ptr() if self.bias is not None else 0, + alpha_vec.data_ptr(), + B, 2, T_new, H, W, Ci, Co, stream) + + if rc != 0: + logger.warning("[fp8_conv3d_mm] kernel rc=%d, falling back to " + "cuDNN for [%d,%d,%d,%d,%d]", rc, B, Ci, T_new, H, W) + padding = list(self._padding) + if cache_x is not None and self._padding[4] > 0: + cx = cache_x.to(x.device) + if not cx.is_contiguous(memory_format=torch.channels_last_3d): + cx = cx.to(memory_format=torch.channels_last_3d) + x_cat = torch.cat([cx, x], dim=2) + padding[4] -= cx.shape[2] + else: + x_cat = x + x_pad = F.pad(x_cat, padding) + return F.conv3d(x_pad, self.weight, self.bias, self.stride, + self.padding, self.dilation, self.groups) + return out + + +# ──────────────────────────────────────────────────────────────────────── +# FP8 fused norm+silu+running-amax+quant → prequant FP8 conv3d +# +# Patches WanResidualBlock.forward so that, for blocks whose conv1 AND +# conv2 are FP8-eligible, the sister norm produces the FP8 activation +# directly via ``fp16_rms_silu_amax_quant_fp8_ndhwc_nozero`` (one kernel: +# RMSNorm + SiLU + accumulate-running-amax + FP8-quant). The conv then +# only has to quantize the small causal cache (2 frames) and reuse the +# prequant FP8 new-activation -- eliminating the large per-call fp16 +# round-trip between norm and conv (the bulk of ``quantize_..._dual``). +# +# Cache correctness: the causal cache (``feat_cache``) stays fp16 (last +# CACHE_T frames of the norm output, computed via a tiny rms_silu on the +# 2-frame slice) so it is re-quantized each call with the current running +# scale -- exactly matching the baseline's shared-scale invariant. +# ──────────────────────────────────────────────────────────────────────── +_FUSE_FP8_NORMQUANT = os.environ.get("FLASHRT_FP8_FUSED_NORMQUANT", "1") == "1" + + +def _make_fp8_fused_residual_forward(orig_forward): + def _patched_forward(self, x, feat_cache=None, feat_idx=[0]): + # Only handle blocks where BOTH convs are FP8 (and NOT promoted to + # NVFP4 — install_vae_nvfp4 sets _nvfp4_w without clearing _fp8_w). + def _is_fp8(c): + return (getattr(c, '_fp8_w', None) is not None + and getattr(c, '_nvfp4_w', None) is None) + c1_fp8 = _is_fp8(self.conv1) + c2_fp8 = _is_fp8(self.conv2) + if not (c1_fp8 and c2_fp8): + return orig_forward(self, x, feat_cache, feat_idx) + + try: + from diffusers.models.autoencoders.autoencoder_kl_wan import CACHE_T + except Exception: + CACHE_T = 2 + stream = torch.cuda.current_stream().cuda_stream + h = self.conv_shortcut(x) # fp16 shortcut, unchanged + + def _to_cl(t): + if not t.is_contiguous(memory_format=torch.channels_last_3d): + return t.to(memory_format=torch.channels_last_3d) + return t + + def _norm_quant_fp8(norm, xin): + """RMSNorm+SiLU+running-amax+FP8-quant -> (new_fp8 flat, amax_buf).""" + B, C, T, Hh, Ww = xin.shape + xin = _to_cl(xin) + gamma_flat, bias_ptr = _prep_gamma_bias(norm.gamma, getattr(norm, 'bias', 0)) + if norm._amax_buf is None or norm._amax_buf.device != xin.device: + norm._amax_buf = torch.zeros(1, dtype=torch.float32, device=xin.device) + n = B * T * Hh * Ww + new_fp8 = torch.empty(n * C, dtype=torch.float8_e4m3fn, device=xin.device) + scale_out = torch.empty(1, dtype=torch.float32, device=xin.device) + rc = _fvk.fp16_rms_silu_amax_quant_fp8_ndhwc_nozero( + xin.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + new_fp8.data_ptr(), scale_out.data_ptr(), + norm._amax_buf.data_ptr(), + B, C, T, Hh, Ww, _EPS, stream) + if rc != 0: + raise RuntimeError(f"[fp8_fused] norm_quant rc={rc}") + return new_fp8, norm._amax_buf + + def _cache_fp16_for_next(norm, xin): + """Tiny rms_silu on last CACHE_T frames -> fp16 cache slice.""" + sl = xin[:, :, -CACHE_T:, :, :] + sl = _to_cl(sl) + B, C, Tc, Hh, Ww = sl.shape + gamma_flat, bias_ptr = _prep_gamma_bias(norm.gamma, getattr(norm, 'bias', 0)) + out = torch.empty_like(sl) + _fvk.fp16_rms_silu_ndhwc( + sl.data_ptr(), gamma_flat.data_ptr(), bias_ptr, + out.data_ptr(), B, C, Tc, Hh, Ww, _EPS, stream) + return out + + def _conv_prequant(conv, new_fp8, amax_buf, shape_in, cache_x_fp16): + B, Ci, T_new, Hh, Ww = shape_in + Co = conv._fp8_w.shape[0] + # resolve cache_2 [B,Ci,2,H,W] channels-last (mirror _fp8_conv3d_forward) + if cache_x_fp16 is not None and cache_x_fp16.shape[2] >= 2: + cache_2 = cache_x_fp16[:, :, -2:] + if not cache_2.is_contiguous(memory_format=torch.channels_last_3d): + cache_2 = cache_2.to(memory_format=torch.channels_last_3d) + elif cache_x_fp16 is not None and cache_x_fp16.shape[2] >= 1: + cache_2 = torch.empty(B, Ci, 2, Hh, Ww, dtype=_FP16, + device=conv._fp8_w.device, + memory_format=torch.channels_last_3d).zero_() + cache_2[:, :, 1:2] = cache_x_fp16[:, :, -1:] + else: + cache_2 = torch.empty(B, Ci, 2, Hh, Ww, dtype=_FP16, + device=conv._fp8_w.device, + memory_format=torch.channels_last_3d).zero_() + n_cache = cache_2.numel() + cache_fp8 = torch.empty(n_cache, dtype=torch.float8_e4m3fn, + device=cache_2.device) + # quant cache with the running amax -> writes conv._fp8_scale + _fvk.quantize_fp16_fp8_with_amax( + cache_2.data_ptr(), cache_fp8.data_ptr(), + amax_buf.data_ptr(), conv._fp8_scale.data_ptr(), + n_cache, stream) + alpha_vec = conv._fp8_scale * conv._w_scale + out = torch.empty(B, Co, T_new, Hh, Ww, dtype=_FP16, + device=new_fp8.device, + memory_format=torch.channels_last_3d) + bias_ptr = conv.bias.data_ptr() if conv.bias is not None else 0 + rc = _fvk.fp8_conv3d_mm_ndhwc_fp16out( + cache_fp8.data_ptr(), new_fp8.data_ptr(), + conv._fp8_w.data_ptr(), out.data_ptr(), + bias_ptr, alpha_vec.data_ptr(), + B, 2, T_new, Hh, Ww, Ci, Co, stream) + if rc != 0: + raise RuntimeError(f"[fp8_fused] conv3d_prequant rc={rc}") + return out + + # ── norm1 + conv1 ────────────────────────────────────────── + x_cl = _to_cl(x) + B0, C0, T0, H0, W0 = x_cl.shape + new_fp8_1, amax_1 = _norm_quant_fp8(self.norm1, x_cl) + cache_in_1 = feat_cache[feat_idx[0]] if feat_cache is not None else None + x_out = _conv_prequant(self.conv1, new_fp8_1, amax_1, + (B0, C0, T0, H0, W0), cache_in_1) + if feat_cache is not None: + feat_cache[feat_idx[0]] = _cache_fp16_for_next(self.norm1, x_cl) + feat_idx[0] += 1 + + # ── norm2 + conv2 ────────────────────────────────────────── + x_out_cl = _to_cl(x_out) + B1, C1, T1, H1, W1 = x_out_cl.shape + new_fp8_2, amax_2 = _norm_quant_fp8(self.norm2, x_out_cl) + cache_in_2 = feat_cache[feat_idx[0]] if feat_cache is not None else None + x_out2 = _conv_prequant(self.conv2, new_fp8_2, amax_2, + (B1, C1, T1, H1, W1), cache_in_2) + if feat_cache is not None: + feat_cache[feat_idx[0]] = _cache_fp16_for_next(self.norm2, x_out_cl) + feat_idx[0] += 1 + + return x_out2 + h + return _patched_forward + + +def _install_fp8_conv3d_pipeline(vae, enabled: bool = True) -> int: + """Pre-quantize applicable Conv3d weights to FP8 e4m3 and patch + WanCausalConv3d.forward to dispatch to the FP8 implicit-GEMM kernel. + + For each 3x3x3 WanCausalConv3d with Ci % 32 == 0 and Co >= 8: + - Pre-quantize weight to FP8 e4m3 with per-output-channel scale. + - Store fp8 weight, scale, and scratch buffers as non-persistent + buffers on the module. + + Non-applicable layers (1x1x1, 3x1x1, Ci%32!=0, Co<8) fall back to + the channels-last cuDNN path. + """ + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanCausalConv3d) + + if not enabled: + return 0 + + n_fp8 = 0 + n_skip = 0 + for m in vae.modules(): + if isinstance(m, WanCausalConv3d): + w_fp8, w_scale = _prequantize_conv3d_weight(m) + if w_fp8 is not None: + device = m.weight.device + m.register_buffer('_fp8_w', w_fp8.to(device), + persistent=False) + m.register_buffer('_w_scale', w_scale.to(device), + persistent=False) + m.register_buffer('_fp8_amax', + torch.empty(1, dtype=torch.float32, + device=device), + persistent=False) + m.register_buffer('_fp8_scale', + torch.empty(1, dtype=torch.float32, + device=device), + persistent=False) + n_fp8 += 1 + else: + m._fp8_w = None + n_skip += 1 + + if n_fp8 == 0: + logger.info("[minimax-vae] FP8 conv3d: 0 layers applicable") + return 0 + + _saved_forward = WanCausalConv3d.forward + + def _fp8_dispatch_forward(self, x, cache_x=None): + if getattr(self, '_fp8_w', None) is not None: + return _fp8_conv3d_forward(self, x, cache_x) + return _saved_forward(self, x, cache_x) + + WanCausalConv3d.forward = _fp8_dispatch_forward + WanCausalConv3d._flashrt_fp8_patched = True + + # ── Wire up sister-norm references for fused amax ─────────── + # Each FP8-enabled conv gets a reference to its sister norm module + # so it can reuse the amax computed by fp16_rms_silu_amax_ndhwc. + # The norm accumulates amax into a running-max buffer (shared with + # the conv) so the conv can skip amax over both x and cache_x. + try: + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanResidualBlock) + n_sister = 0 + for blk in vae.modules(): + if isinstance(blk, WanResidualBlock): + if getattr(blk.conv1, '_fp8_w', None) is not None: + norm1 = blk.norm1 + if hasattr(norm1, '_fp8_sister_conv'): + norm1._fp8_sister_conv = blk.conv1 + blk.conv1._sister_norm = norm1 + norm1._running_mode = True + n_sister += 1 + if getattr(blk.conv2, '_fp8_w', None) is not None: + norm2 = blk.norm2 + if hasattr(norm2, '_fp8_sister_conv'): + norm2._fp8_sister_conv = blk.conv2 + blk.conv2._sister_norm = norm2 + norm2._running_mode = True + n_sister += 1 + if n_sister > 0: + logger.info("[minimax-vae] fused norm+silu+amax + running-max: " + "%d norm→conv links (skips amax over cache_x entirely)", + n_sister) + except Exception as e: + logger.debug("[minimax-vae] sister-norm link skipped: %s", e) + + # ── FP8 fused norm+silu+running-amax+quant at residual-block level ── + # For blocks where BOTH conv1+conv2 are FP8-eligible, patch the block + # forward to produce the FP8 activation directly in the norm (one + # kernel) and feed it pre-quantized to the conv (which only re-quants + # the small causal cache). Eliminates the large fp16 round-trip. + if _FUSE_FP8_NORMQUANT: + try: + from diffusers.models.autoencoders.autoencoder_kl_wan import ( + WanResidualBlock) + _orig_res_fwd = WanResidualBlock.forward + _patched = _make_fp8_fused_residual_forward(_orig_res_fwd) + n_blk = 0 + for blk in vae.modules(): + if isinstance(blk, WanResidualBlock): + def _is_fp8(c): + return (getattr(c, '_fp8_w', None) is not None + and getattr(c, '_nvfp4_w', None) is None) + if _is_fp8(blk.conv1) and _is_fp8(blk.conv2): + n_blk += 1 + if n_blk > 0: + WanResidualBlock.forward = _patched + logger.info("[minimax-vae] FP8 fused norm+silu+quant: " + "%d WanResidualBlocks (prequant FP8 conv path)", + n_blk) + except Exception as e: + logger.warning("[minimax-vae] FP8 fused residual patch failed: %s", e) + + logger.info("[minimax-vae] FP8 implicit-GEMM conv3d: %d layers " + "quantized, %d skipped (non-3x3x3 or Ci%%32!=0)", + n_fp8, n_skip) + return n_fp8 + + + + +@torch.no_grad() +def profile_vae(pipe, images_tensor, masks_infer, height, width, num_frames, + iterations=6, num_inference_steps=12, seed=42, + device=torch.device("cuda:0")) -> Dict[str, float]: + """Time VAE encode + transformer denoise + VAE decode separately.""" + from einops import rearrange + from diffusers.utils.torch_utils import randn_tensor + + vae = pipe.vae + transformer = pipe.transformer + scheduler = pipe.scheduler + + scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = scheduler.timesteps + num_channels_latents = 16 + vae_scale_factor_temporal = pipe.vae_scale_factor_temporal + vae_scale_factor_spatial = pipe.vae_scale_factor_spatial + num_latent_frames = (num_frames - 1) // vae_scale_factor_temporal + 1 + + shape = (1, num_channels_latents, num_latent_frames, + height // vae_scale_factor_spatial, + width // vae_scale_factor_spatial) + generator = torch.Generator(device=device).manual_seed(seed) + latents = randn_tensor(shape, generator=generator, device=device, + dtype=torch.float16) + + masks = pipe.expand_masks(masks_infer, iterations) + masks = pipe.resize(masks, height, width).to(device).half() + masks[masks > 0] = 1 + images = rearrange(images_tensor, "f h w c -> c f h w") + images = pipe.resize(images[None, ...], height, width).to(device).half() + masked_images = images * (1 - masks) + + latents_mean = (torch.tensor(vae.config.latents_mean) + .view(1, vae.config.z_dim, 1, 1, 1) + .to(vae.device, torch.float16)) + latents_std = 1.0 / torch.tensor(vae.config.latents_std).view( + 1, vae.config.z_dim, 1, 1, 1).to(vae.device, torch.float16) + + vae_dtype = next(vae.parameters()).dtype + + torch.cuda.synchronize() + ev0 = torch.cuda.Event(enable_timing=True) + ev_enc = torch.cuda.Event(enable_timing=True) + ev0.record() + masked_latents = vae.encode(masked_images.to(vae_dtype)).latent_dist.mode() + masks_latents = vae.encode((2 * masks - 1.0).to(vae_dtype)).latent_dist.mode() + masked_latents = (masked_latents - latents_mean) * latents_std + masks_latents = (masks_latents - latents_mean) * latents_std + ev_enc.record() + torch.cuda.synchronize() + vae_encode_ms = ev0.elapsed_time(ev_enc) + + ev_denoise_start = torch.cuda.Event(enable_timing=True) + ev_denoise_end = torch.cuda.Event(enable_timing=True) + ev_denoise_start.record() + for i, t in enumerate(timesteps): + latent_model_input = latents.to(torch.float16) + latent_model_input = torch.cat( + [latent_model_input, masked_latents, masks_latents], dim=1) + timestep = t.expand(latents.shape[0]) + noise_pred = transformer( + hidden_states=latent_model_input.half(), timestep=timestep)[0] + latents = scheduler.step(noise_pred, t, latents, + return_dict=False)[0] + ev_denoise_end.record() + torch.cuda.synchronize() + denoise_ms = ev_denoise_start.elapsed_time(ev_denoise_end) + + latents = latents.half() / latents_std + latents_mean + ev_dec_start = torch.cuda.Event(enable_timing=True) + ev_dec_end = torch.cuda.Event(enable_timing=True) + ev_dec_start.record() + video = vae.decode(latents.to(vae_dtype), return_dict=False)[0] + ev_dec_end.record() + torch.cuda.synchronize() + vae_decode_ms = ev_dec_start.elapsed_time(ev_dec_end) + + return { + "vae_encode_ms": vae_encode_ms, + "denoise_ms": denoise_ms, + "vae_decode_ms": vae_decode_ms, + "total_ms": vae_encode_ms + denoise_ms + vae_decode_ms, + } diff --git a/tests/test_minimax_remover_smoke.py b/tests/test_minimax_remover_smoke.py index 03bad5bc..89ebe1f0 100644 --- a/tests/test_minimax_remover_smoke.py +++ b/tests/test_minimax_remover_smoke.py @@ -266,18 +266,60 @@ def test_fp8_pipeline_call_does_not_patch_pipe_class(monkeypatch): """Wrapping one FP8 pipe must not alter all instances of that pipe class.""" from flash_rt.models.minimax_remover import _fp8_pipeline + # Exercise the delegation path (orig pipe __call__) rather than the + # eager-manual denoise default, so the stub does not need a real + # transformer/scheduler. The class-isolation guarantee under test is + # independent of the steady-state dispatch mode. + monkeypatch.setenv("FLASHRT_FP8_EAGER_MANUAL", "0") + + class _Param: + dtype = "fp16" + class _Transformer: + def __init__(self): + self.config = types.SimpleNamespace(eps=1e-6) + self._hooks = [] + def to(self, _dtype): return self + def parameters(self): + return iter([_Param()]) + + def register_forward_hook(self, fn): + self._hooks.append(fn) + + class _Handle: + def __init__(self, hooks, f): + self._hooks = hooks + self._f = f + + def remove(self): + if self._f in self._hooks: + self._hooks.remove(self._f) + + return _Handle(self._hooks, fn) + + def _fire_hooks(self): + for fn in list(self._hooks): + fn(self, None, None) + + class _Vae: + def parameters(self): + return iter([_Param()]) + class _CallablePipe: def __init__(self, name): self.name = name self.transformer = _Transformer() + self.vae = _Vae() self.calls = [] def __call__(self, *args, **kwargs): self.calls.append((args, kwargs)) + # Simulate the transformer forward so the one-shot calibration + # freeze hook fires during the wrapped pipe's first call. + self.transformer._fire_hooks() return self.name, args, kwargs set_calibration_calls = [] @@ -327,6 +369,12 @@ def install_fa2_attention(_transformer): assert set_calibration_calls == [True] assert freeze_calls == [1.1] + assert wrapped._calibrated + + assert wrapped("again") == ("pipe1", ("again",), {}) + assert set_calibration_calls == [True] + assert freeze_calls == [1.1] + # ── 4. Gated build: required symbols present and callable ──