[diffusion] model: support VDN-H3 with a hybrid_window_attn_h3 backend (#37903)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Haocheng Xi <xihc@berkeley.edu> Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Haocheng Xi
Mick
parent
e91c948057
commit
ff1ce11348
@@ -47,8 +47,22 @@ struct QKNormRopePackKVParams : QKNormRopeParams {
|
||||
uint32_t suffix_tokens;
|
||||
};
|
||||
|
||||
template <bool kPackKV>
|
||||
using QKNormRopeParamsT = std::conditional_t<kPackKV, QKNormRopePackKVParams, QKNormRopeParams>;
|
||||
/// \brief Out-of-place variant: q/k are read (any strides), the normed + roped
|
||||
/// rows are written to q_out/k_out and the inputs stay untouched. Used where a
|
||||
/// consumer still needs the raw projections (VDN-H3's NoPE linear branch).
|
||||
struct QKNormRopeOutOfPlaceParams : QKNormRopeParams {
|
||||
void* __restrict__ q_out_ptr;
|
||||
void* __restrict__ k_out_ptr; // pre-offset by -num_qo_heads * out_head_stride_bytes
|
||||
int64_t q_out_stride_bytes;
|
||||
int64_t k_out_stride_bytes;
|
||||
int64_t out_head_stride_bytes;
|
||||
};
|
||||
|
||||
template <bool kPackKV, bool kOutOfPlace = false>
|
||||
using QKNormRopeParamsT = std::conditional_t<
|
||||
kPackKV,
|
||||
QKNormRopePackKVParams,
|
||||
std::conditional_t<kOutOfPlace, QKNormRopeOutOfPlaceParams, QKNormRopeParams>>;
|
||||
|
||||
constexpr uint32_t kThreadsPerBlock = 256;
|
||||
constexpr uint32_t kWarpsPerBlock = kThreadsPerBlock / device::kWarpThreads;
|
||||
@@ -199,9 +213,11 @@ template <
|
||||
bool kRoundNormBeforeRope,
|
||||
bool kPackKV,
|
||||
bool kCacheHasFullWidth,
|
||||
typename IdType>
|
||||
__global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_constant__ params) {
|
||||
typename IdType,
|
||||
bool kOutOfPlace = false>
|
||||
__global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV, kOutOfPlace> __grid_constant__ params) {
|
||||
using namespace device;
|
||||
static_assert(!(kPackKV && kOutOfPlace), "KV packing and out-of-place output are exclusive");
|
||||
|
||||
static_assert(std::is_same_v<DType, fp16_t> || std::is_same_v<DType, bf16_t>);
|
||||
static_assert(kHeadDim <= 256, "Only warp-level fused qknorm+rope is supported");
|
||||
@@ -291,6 +307,13 @@ __global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_c
|
||||
const void* input = load_q ? pointer::offset(q_ptr, token_id * q_stride_bytes, head_id * head_stride_bytes)
|
||||
: pointer::offset(k_ptr, token_id * k_stride_bytes, head_id * head_stride_bytes);
|
||||
void* output = const_cast<void*>(input);
|
||||
if constexpr (kOutOfPlace) {
|
||||
output =
|
||||
load_q ? pointer::offset(
|
||||
params.q_out_ptr, token_id * params.q_out_stride_bytes, head_id * params.out_head_stride_bytes)
|
||||
: pointer::offset(
|
||||
params.k_out_ptr, token_id * params.k_out_stride_bytes, head_id * params.out_head_stride_bytes);
|
||||
}
|
||||
if constexpr (kPackKV) {
|
||||
if (!load_q) {
|
||||
const uint32_t batch_id = token_id / params.suffix_tokens;
|
||||
@@ -446,6 +469,23 @@ __global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_c
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
}
|
||||
|
||||
/// \brief Shared launch tail of the three host runners: pick the index-type
|
||||
/// instantiation, size the persistent grid from the occupancy table, launch.
|
||||
template <auto kKernelI32, auto kKernelI64, typename Params>
|
||||
void launch_qknorm_rope(const Params& params, bool is_int32, uint32_t num_works, DLDevice device, bool use_pdl) {
|
||||
using namespace host;
|
||||
const auto selected_kernel = is_int32 ? kKernelI32 : kKernelI64;
|
||||
const uint32_t kNumSM = runtime::get_sm_count(device.device_id);
|
||||
static const uint32_t kOccupancyTable[2] = {
|
||||
runtime::get_blocks_per_sm(kKernelI32, kThreadsPerBlock),
|
||||
runtime::get_blocks_per_sm(kKernelI64, kThreadsPerBlock),
|
||||
};
|
||||
const auto max_blocks = kOccupancyTable[is_int32 ? 0 : 1] * kNumSM;
|
||||
const auto needed_blocks = div_ceil(num_works, kWarpsPerBlock);
|
||||
const auto num_blocks = std::min(max_blocks, needed_blocks);
|
||||
LaunchKernel(num_blocks, kThreadsPerBlock, device).enable_pdl(use_pdl)(selected_kernel, params);
|
||||
}
|
||||
|
||||
template <
|
||||
int64_t kHeadDim,
|
||||
int64_t kRopeDim,
|
||||
@@ -528,18 +568,106 @@ struct QKNormRopeKernel {
|
||||
.eps = eps,
|
||||
};
|
||||
|
||||
const auto is_int32 = id_type.is_type<int32_t>();
|
||||
const auto selected_kernel = is_int32 ? kernel<int32_t> : kernel<int64_t>;
|
||||
const uint32_t kNumSM = runtime::get_sm_count(device.unwrap().device_id);
|
||||
static const uint32_t kOccupancyTable[2] = {
|
||||
runtime::get_blocks_per_sm(kernel<int32_t>, kThreadsPerBlock),
|
||||
runtime::get_blocks_per_sm(kernel<int64_t>, kThreadsPerBlock),
|
||||
};
|
||||
const auto max_blocks = kOccupancyTable[is_int32 ? 0 : 1] * kNumSM;
|
||||
const auto num_works = (num_qo_heads + num_kv_heads) * num_tokens;
|
||||
const auto needed_blocks = div_ceil(num_works, kWarpsPerBlock);
|
||||
const auto num_blocks = std::min(max_blocks, needed_blocks);
|
||||
LaunchKernel(num_blocks, kThreadsPerBlock, device.unwrap()).enable_pdl(kUsePDL)(selected_kernel, params);
|
||||
launch_qknorm_rope<kernel<int32_t>, kernel<int64_t>>(
|
||||
params, id_type.is_type<int32_t>(), num_works, device.unwrap(), kUsePDL);
|
||||
}
|
||||
};
|
||||
|
||||
template <
|
||||
int64_t kHeadDim,
|
||||
int64_t kRopeDim,
|
||||
bool kIsNeox,
|
||||
bool kUsePDL,
|
||||
typename DType,
|
||||
typename CacheDType,
|
||||
bool kRoundNormBeforeRope,
|
||||
bool kCacheHasFullWidth>
|
||||
struct QKNormRopeOutOfPlaceKernel {
|
||||
static_assert(kHeadDim <= 256, "Only head_dim <= 256 is supported");
|
||||
template <typename IdType>
|
||||
static constexpr auto kernel = fused_qknorm_rope_warp<
|
||||
kHeadDim,
|
||||
kRopeDim,
|
||||
kIsNeox,
|
||||
kUsePDL,
|
||||
DType,
|
||||
CacheDType,
|
||||
kRoundNormBeforeRope,
|
||||
false,
|
||||
kCacheHasFullWidth,
|
||||
IdType,
|
||||
true>;
|
||||
|
||||
/// \brief QK-norm + RoPE from q/k into q_out/k_out; q and k are left untouched.
|
||||
static void
|
||||
run(const tvm::ffi::TensorView q,
|
||||
const tvm::ffi::TensorView k,
|
||||
const tvm::ffi::TensorView q_out,
|
||||
const tvm::ffi::TensorView k_out,
|
||||
const tvm::ffi::TensorView q_weight,
|
||||
const tvm::ffi::TensorView k_weight,
|
||||
const tvm::ffi::TensorView cos_sin_cache,
|
||||
const tvm::ffi::TensorView positions,
|
||||
float eps) {
|
||||
using namespace host;
|
||||
|
||||
auto N = SymbolicSize{"num_tokens"};
|
||||
auto Q = SymbolicSize{"num_qo_heads"};
|
||||
auto K = SymbolicSize{"num_kv_heads"};
|
||||
auto D = SymbolicSize{"head_dim"};
|
||||
auto Dq = SymbolicSize{"q_stride"};
|
||||
auto Dk = SymbolicSize{"k_stride"};
|
||||
auto Dd = SymbolicSize{"head_stride"};
|
||||
auto Dqo = SymbolicSize{"q_out_stride"};
|
||||
auto Dko = SymbolicSize{"k_out_stride"};
|
||||
auto Ddo = SymbolicSize{"out_head_stride"};
|
||||
auto device = SymbolicDevice{};
|
||||
auto id_type = SymbolicDType{};
|
||||
D.set_value(kHeadDim);
|
||||
device.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({N, Q, D}).with_strides({Dq, Dd, 1}).with_dtype<DType>().with_device(device).verify(q);
|
||||
TensorMatcher({N, K, D}).with_strides({Dk, Dd, 1}).with_dtype<DType>().with_device(device).verify(k);
|
||||
TensorMatcher({N, Q, D}).with_strides({Dqo, Ddo, 1}).with_dtype<DType>().with_device(device).verify(q_out);
|
||||
TensorMatcher({N, K, D}).with_strides({Dko, Ddo, 1}).with_dtype<DType>().with_device(device).verify(k_out);
|
||||
TensorMatcher({D}).with_dtype<DType>().with_device(device).verify(q_weight).verify(k_weight);
|
||||
TensorMatcher({-1, kCacheHasFullWidth ? 2 * kRopeDim : kRopeDim})
|
||||
.with_dtype<CacheDType>()
|
||||
.with_device(device)
|
||||
.verify(cos_sin_cache);
|
||||
TensorMatcher({N}).with_dtype<int32_t, int64_t>(id_type).with_device(device).verify(positions);
|
||||
|
||||
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
|
||||
const auto num_qo_heads = static_cast<uint32_t>(Q.unwrap());
|
||||
const auto num_kv_heads = static_cast<uint32_t>(K.unwrap());
|
||||
if (num_tokens == 0 || (num_qo_heads == 0 && num_kv_heads == 0)) return;
|
||||
const auto head_stride_bytes = static_cast<int64_t>(Dd.unwrap() * sizeof(DType));
|
||||
const auto out_head_stride_bytes = static_cast<int64_t>(Ddo.unwrap() * sizeof(DType));
|
||||
|
||||
QKNormRopeOutOfPlaceParams params{};
|
||||
params.q_ptr = q.data_ptr();
|
||||
params.k_ptr = pointer::offset(k.data_ptr(), -static_cast<int64_t>(num_qo_heads) * head_stride_bytes);
|
||||
params.q_weight_ptr = q_weight.data_ptr();
|
||||
params.k_weight_ptr = k_weight.data_ptr();
|
||||
params.cos_sin_cache_ptr = cos_sin_cache.data_ptr();
|
||||
params.positions = positions.data_ptr();
|
||||
params.q_stride_bytes = static_cast<int64_t>(Dq.unwrap() * sizeof(DType));
|
||||
params.k_stride_bytes = static_cast<int64_t>(Dk.unwrap() * sizeof(DType));
|
||||
params.head_stride_bytes = head_stride_bytes;
|
||||
params.num_qo_heads = num_qo_heads;
|
||||
params.num_kv_heads = num_kv_heads;
|
||||
params.num_tokens = num_tokens;
|
||||
params.eps = eps;
|
||||
params.q_out_ptr = q_out.data_ptr();
|
||||
params.k_out_ptr = pointer::offset(k_out.data_ptr(), -static_cast<int64_t>(num_qo_heads) * out_head_stride_bytes);
|
||||
params.q_out_stride_bytes = static_cast<int64_t>(Dqo.unwrap() * sizeof(DType));
|
||||
params.k_out_stride_bytes = static_cast<int64_t>(Dko.unwrap() * sizeof(DType));
|
||||
params.out_head_stride_bytes = out_head_stride_bytes;
|
||||
|
||||
const auto num_works = (num_qo_heads + num_kv_heads) * num_tokens;
|
||||
launch_qknorm_rope<kernel<int32_t>, kernel<int64_t>>(
|
||||
params, id_type.is_type<int32_t>(), num_works, device.unwrap(), kUsePDL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -655,20 +783,11 @@ struct QKNormRopePackKVKernel {
|
||||
params.prefix_tokens = static_cast<uint32_t>(prefix_tokens);
|
||||
params.suffix_tokens = static_cast<uint32_t>(suffix_tokens);
|
||||
|
||||
const auto is_int32 = id_type.is_type<int32_t>();
|
||||
const auto selected_kernel = is_int32 ? kernel<int32_t> : kernel<int64_t>;
|
||||
const uint32_t kNumSM = runtime::get_sm_count(device.unwrap().device_id);
|
||||
static const uint32_t kOccupancyTable[2] = {
|
||||
runtime::get_blocks_per_sm(kernel<int32_t>, kThreadsPerBlock),
|
||||
runtime::get_blocks_per_sm(kernel<int64_t>, kThreadsPerBlock),
|
||||
};
|
||||
const auto max_blocks = kOccupancyTable[is_int32 ? 0 : 1] * kNumSM;
|
||||
const uint32_t num_prefix_works = static_cast<uint32_t>(batch_size * prefix_tokens) * num_kv_heads;
|
||||
const uint32_t num_works =
|
||||
(num_qo_heads + num_kv_heads) * num_tokens + 2 * num_prefix_works + num_tokens * num_kv_heads;
|
||||
const auto needed_blocks = div_ceil(num_works, kWarpsPerBlock);
|
||||
const auto num_blocks = std::min(max_blocks, needed_blocks);
|
||||
LaunchKernel(num_blocks, kThreadsPerBlock, device.unwrap()).enable_pdl(kUsePDL)(selected_kernel, params);
|
||||
launch_qknorm_rope<kernel<int32_t>, kernel<int64_t>>(
|
||||
params, id_type.is_type<int32_t>(), num_works, device.unwrap(), kUsePDL);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Fused VDN-H3 delta-rule factors.
|
||||
//
|
||||
// Per (frame, head) the linear branch needs, for M = I + A (128x128 fp32, symmetric positive definite):
|
||||
// transition = diag(alpha) M^-1 [F, H, dk, dk]
|
||||
// injection = B M^-1 [F, H, dv, dk]
|
||||
// The eager path is cholesky + solve_triangular + two GEMMs (~40 launches); this kernel is one
|
||||
// launch: one CTA of 256 threads per matrix, thread (ti, tj) owns rows 8ti.., cols 8tj.. as float2
|
||||
// pairs t[8][4] so the rank-2 updates and the final GEMM run on packed FFMA2 (sm_100+, scalar
|
||||
// fallback elsewhere).
|
||||
//
|
||||
// M^-1 is formed in place by block Gauss-Jordan elimination without pivoting (stable for SPD, the
|
||||
// same class as Cholesky), two pivots per barrier. For the pivot block S = {k, k+1} with
|
||||
// P = M[S,S], R = M[S,:], C = M[:,S], D the rest:
|
||||
// R' = P^-1 R, G = C (raw), D' = D - G R', M'[~S, S] = 0 - G R'[:,S] = -G P^-1
|
||||
// (in-place GJ inverse semantics: the eliminated column receives the inverse column). The next
|
||||
// block's band is prepared in the middle of the current update (software pipelined) and the barrier
|
||||
// sits before the second half of the update so its FMAs overlap the barrier skew and the loads of
|
||||
// the next step. Shared rows are column-swizzled so the float4 reads are bank-conflict free; the
|
||||
// same swizzle is used for the smem copy of M^-1 consumed by the register-tiled GEMM B M^-1.
|
||||
//
|
||||
// Accuracy: the relative error vs fp64 matches the cholesky path; both are dominated by cond(M).
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace sglang {
|
||||
|
||||
namespace vdn_delta_factors {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDim = 128; // dk == dv == 128
|
||||
constexpr int kBlockSize = 256; // 16 x 16 tiles of 8 x 8
|
||||
constexpr int kMinBlocksPerSm = 2; // 128 registers per thread
|
||||
constexpr int kXsBytes = kDim * kDim * static_cast<int>(sizeof(float)); // 64 KB dynamic smem
|
||||
constexpr unsigned kFullMask = 0xffffffffu;
|
||||
|
||||
// thread tj's tile columns 8tj..8tj+3 land at 4tj.., 8tj+4..8tj+7 at 64+4tj.., so a quarter warp
|
||||
// reads 8 consecutive 16-byte chunks
|
||||
SGL_DEVICE int swz_lo(int tj) {
|
||||
return 4 * tj;
|
||||
}
|
||||
SGL_DEVICE int swz_hi(int tj) {
|
||||
return 64 + 4 * tj;
|
||||
}
|
||||
|
||||
SGL_DEVICE float rcp_nr(float p) {
|
||||
float r;
|
||||
asm("rcp.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(p));
|
||||
return fmaf(r, fmaf(-p, r, 1.f), r); // one Newton step: ~0.5 ulp
|
||||
}
|
||||
SGL_DEVICE float4 ld4(const float* p) {
|
||||
return *reinterpret_cast<const float4*>(p);
|
||||
}
|
||||
SGL_DEVICE void st4(float* p, float a, float b, float c, float d) {
|
||||
*reinterpret_cast<float4*>(p) = make_float4(a, b, c, d);
|
||||
}
|
||||
SGL_DEVICE void st4(float* p, float2 a, float2 b) {
|
||||
*reinterpret_cast<float4*>(p) = make_float4(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
SGL_DEVICE float2 f2(float a) {
|
||||
return make_float2(a, a);
|
||||
}
|
||||
// packed fma: (a.x*b.x+c.x, a.y*b.y+c.y); one FFMA2 on sm_100+ (the first operand is a scalar
|
||||
// broadcast in SASS), two FFMA elsewhere. Bitwise identical results either way.
|
||||
SGL_DEVICE float2 fma2(float2 a, float2 b, float2 c) {
|
||||
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000)
|
||||
return __ffma2_rn(a, b, c);
|
||||
#else
|
||||
return make_float2(fmaf(a.x, b.x, c.x), fmaf(a.y, b.y, c.y));
|
||||
#endif
|
||||
}
|
||||
SGL_DEVICE float get(const float4& v, int i) {
|
||||
return i == 0 ? v.x : i == 1 ? v.y : i == 2 ? v.z : v.w;
|
||||
}
|
||||
|
||||
struct Smem {
|
||||
float row[2][2][kDim]; // [buffer][band row m][swizzled col] scaled band rows R'
|
||||
float col[2][kDim * 2]; // [buffer][row*2 + m] multipliers G
|
||||
};
|
||||
|
||||
/// Prepare the band of pivot block {k, k+1}, k = 8*kt1 + R1 (R1 even). Column owners (tj == kt1)
|
||||
/// publish G = C (0 for the band rows) and zero their band columns; row owners (ti == kt1) replace P
|
||||
/// by I, scale R' = P^-1 R and publish it. Executed by all threads (P is broadcast with shuffles).
|
||||
template <int R1, int BUF>
|
||||
SGL_DEVICE void prepare(float2 (&t)[8][4], Smem& sm, int kt1, int ti, int tj, int i0) {
|
||||
constexpr int CP = R1 >> 1;
|
||||
const int src = ((kt1 & 1) << 4) | kt1; // lane of the diagonal tile inside the row-owner warp
|
||||
const float pa = __shfl_sync(kFullMask, t[R1][CP].x, src);
|
||||
const float pb = __shfl_sync(kFullMask, t[R1][CP].y, src);
|
||||
const float pc = __shfl_sync(kFullMask, t[R1 + 1][CP].x, src);
|
||||
const float pd = __shfl_sync(kFullMask, t[R1 + 1][CP].y, src);
|
||||
if (tj == kt1) {
|
||||
const bool diag = (ti == kt1);
|
||||
float* dst = &sm.col[BUF][i0 * 2];
|
||||
float2 g[8];
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
const bool band = diag && (r == R1 || r == R1 + 1);
|
||||
g[r] = band ? make_float2(0.f, 0.f) : t[r][CP];
|
||||
if (!band) t[r][CP] = make_float2(0.f, 0.f);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int q = 0; q < 4; ++q)
|
||||
st4(dst + 4 * q, g[2 * q], g[2 * q + 1]);
|
||||
}
|
||||
if (ti == kt1) {
|
||||
const float det = fmaf(pa, pd, -pb * pc);
|
||||
const float idet = rcp_nr(det);
|
||||
const float ia = pd * idet, ib = -pb * idet, ic = -pc * idet, id = pa * idet;
|
||||
if (tj == kt1) {
|
||||
t[R1][CP] = make_float2(1.f, 0.f);
|
||||
t[R1 + 1][CP] = make_float2(0.f, 1.f);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int cp = 0; cp < 4; ++cp) {
|
||||
const float2 x = t[R1][cp], y = t[R1 + 1][cp];
|
||||
t[R1][cp] = make_float2(fmaf(ia, x.x, ib * y.x), fmaf(ia, x.y, ib * y.y));
|
||||
t[R1 + 1][cp] = make_float2(fmaf(ic, x.x, id * y.x), fmaf(ic, x.y, id * y.y));
|
||||
}
|
||||
st4(&sm.row[BUF][0][swz_lo(tj)], t[R1][0], t[R1][1]);
|
||||
st4(&sm.row[BUF][0][swz_hi(tj)], t[R1][2], t[R1][3]);
|
||||
st4(&sm.row[BUF][1][swz_lo(tj)], t[R1 + 1][0], t[R1 + 1][1]);
|
||||
st4(&sm.row[BUF][1][swz_hi(tj)], t[R1 + 1][2], t[R1 + 1][3]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-2 update for pivots (8kt + KR, 8kt + KR + 1); the next block's band is prepared in the middle.
|
||||
template <int KR>
|
||||
SGL_DEVICE void step(float2 (&t)[8][4], Smem& sm, int kt, int ti, int tj, int i0) {
|
||||
constexpr int CUR = (KR >> 1) & 1, NXT = CUR ^ 1;
|
||||
constexpr int R1 = (KR + 2) & 7, CP = R1 >> 1; // next block's band rows / column pair
|
||||
float2 rk0[4], rk1[4];
|
||||
float ck0[8], ck1[8]; // negated multipliers (scalar broadcast operands of FFMA2)
|
||||
{
|
||||
const float4 a = ld4(&sm.row[CUR][0][swz_lo(tj)]), b = ld4(&sm.row[CUR][0][swz_hi(tj)]);
|
||||
const float4 c = ld4(&sm.row[CUR][1][swz_lo(tj)]), d = ld4(&sm.row[CUR][1][swz_hi(tj)]);
|
||||
rk0[0] = make_float2(a.x, a.y);
|
||||
rk0[1] = make_float2(a.z, a.w);
|
||||
rk0[2] = make_float2(b.x, b.y);
|
||||
rk0[3] = make_float2(b.z, b.w);
|
||||
rk1[0] = make_float2(c.x, c.y);
|
||||
rk1[1] = make_float2(c.z, c.w);
|
||||
rk1[2] = make_float2(d.x, d.y);
|
||||
rk1[3] = make_float2(d.z, d.w);
|
||||
const float* cp = &sm.col[CUR][i0 * 2];
|
||||
#pragma unroll
|
||||
for (int q = 0; q < 4; ++q) {
|
||||
const float4 v = ld4(cp + 4 * q);
|
||||
ck0[2 * q] = -v.x;
|
||||
ck1[2 * q] = -v.y;
|
||||
ck0[2 * q + 1] = -v.z;
|
||||
ck1[2 * q + 1] = -v.w;
|
||||
}
|
||||
}
|
||||
// part 1: the next band (rows R1, R1+1 fully; column pair CP in the other rows)
|
||||
#pragma unroll
|
||||
for (int cp = 0; cp < 4; ++cp) {
|
||||
t[R1][cp] = fma2(f2(ck1[R1]), rk1[cp], fma2(f2(ck0[R1]), rk0[cp], t[R1][cp]));
|
||||
t[R1 + 1][cp] = fma2(f2(ck1[R1 + 1]), rk1[cp], fma2(f2(ck0[R1 + 1]), rk0[cp], t[R1 + 1][cp]));
|
||||
}
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
if (r == R1 || r == R1 + 1) continue;
|
||||
t[r][CP] = fma2(f2(ck1[r]), rk1[CP], fma2(f2(ck0[r]), rk0[CP], t[r][CP]));
|
||||
}
|
||||
if (KR != 6 || kt != 15) prepare<R1, NXT>(t, sm, kt + (KR == 6 ? 1 : 0), ti, tj, i0);
|
||||
__syncthreads();
|
||||
// part 2: everything else (registers only; overlaps the barrier skew and the next step's loads)
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
if (r == R1 || r == R1 + 1) continue;
|
||||
#pragma unroll
|
||||
for (int cp = 0; cp < 4; ++cp) {
|
||||
if (cp == CP) continue;
|
||||
t[r][cp] = fma2(f2(ck1[r]), rk1[cp], fma2(f2(ck0[r]), rk0[cp], t[r][cp]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief transition = diag(alpha) (I + A)^-1, injection = B (I + A)^-1 for a batch of 128x128 SPD A.
|
||||
*
|
||||
* \param A [N, 128, 128] fp32, symmetric positive semi-definite (I + A is inverted)
|
||||
* \param B [N, 128, 128] fp32
|
||||
* \param alpha [N, 128] fp32 row scales of the transition
|
||||
* \param transition [N, 128, 128] fp32 output
|
||||
* \param injection [N, 128, 128] fp32 output
|
||||
*/
|
||||
__global__ void __launch_bounds__(kBlockSize, kMinBlocksPerSm) vdn_delta_factors_kernel(
|
||||
const float* __restrict__ A,
|
||||
const float* __restrict__ B,
|
||||
const float* __restrict__ alpha,
|
||||
float* __restrict__ transition,
|
||||
float* __restrict__ injection) {
|
||||
extern __shared__ __align__(16) float Xs[]; // [kDim][kDim], column-swizzled copy of (I + A)^-1
|
||||
__shared__ __align__(16) Smem sm;
|
||||
|
||||
const int n = blockIdx.x;
|
||||
const int tid = threadIdx.x;
|
||||
const int ti = tid >> 4, tj = tid & 15;
|
||||
const int i0 = ti * 8, j0 = tj * 8;
|
||||
const size_t mat = static_cast<size_t>(n) * kDim * kDim;
|
||||
const float* An = A + mat;
|
||||
const float* Bn = B + mat;
|
||||
|
||||
float2 t[8][4];
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
const float4 v0 = ld4(An + (i0 + r) * kDim + j0), v1 = ld4(An + (i0 + r) * kDim + j0 + 4);
|
||||
t[r][0] = make_float2(v0.x, v0.y);
|
||||
t[r][1] = make_float2(v0.z, v0.w);
|
||||
t[r][2] = make_float2(v1.x, v1.y);
|
||||
t[r][3] = make_float2(v1.z, v1.w);
|
||||
}
|
||||
if (ti == tj) { // M = I + A
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
if (r & 1) {
|
||||
t[r][r >> 1].y += 1.f;
|
||||
} else {
|
||||
t[r][r >> 1].x += 1.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepare<0, 0>(t, sm, 0, ti, tj, i0);
|
||||
__syncthreads();
|
||||
#pragma unroll 1
|
||||
for (int kt = 0; kt < kDim / 8; ++kt) {
|
||||
step<0>(t, sm, kt, ti, tj, i0);
|
||||
step<2>(t, sm, kt, ti, tj, i0);
|
||||
step<4>(t, sm, kt, ti, tj, i0);
|
||||
step<6>(t, sm, kt, ti, tj, i0);
|
||||
}
|
||||
|
||||
// transition = diag(alpha) X, straight from registers
|
||||
{
|
||||
const float* al = alpha + static_cast<size_t>(n) * kDim + i0;
|
||||
const float4 a0 = ld4(al), a1 = ld4(al + 4);
|
||||
const float av[8] = {a0.x, a0.y, a0.z, a0.w, a1.x, a1.y, a1.z, a1.w};
|
||||
float* Tn = transition + mat;
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
float* row = Tn + (i0 + r) * kDim + j0;
|
||||
st4(row, av[r] * t[r][0].x, av[r] * t[r][0].y, av[r] * t[r][1].x, av[r] * t[r][1].y);
|
||||
st4(row + 4, av[r] * t[r][2].x, av[r] * t[r][2].y, av[r] * t[r][3].x, av[r] * t[r][3].y);
|
||||
}
|
||||
}
|
||||
// stage X in smem (swizzled columns) for the GEMM
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
float* row = Xs + (i0 + r) * kDim;
|
||||
st4(row + swz_lo(tj), t[r][0], t[r][1]);
|
||||
st4(row + swz_hi(tj), t[r][2], t[r][3]);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// injection = B X (register-tiled GEMM; B rows from global through L1, X from smem)
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r)
|
||||
#pragma unroll
|
||||
for (int cp = 0; cp < 4; ++cp)
|
||||
t[r][cp] = make_float2(0.f, 0.f);
|
||||
const float* Bp = Bn + i0 * kDim;
|
||||
#pragma unroll 1
|
||||
for (int k = 0; k < kDim; k += 4) {
|
||||
float4 b[8];
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r)
|
||||
b[r] = __ldg(reinterpret_cast<const float4*>(Bp + r * kDim + k));
|
||||
#pragma unroll
|
||||
for (int kk = 0; kk < 4; ++kk) {
|
||||
const float* xr = Xs + (k + kk) * kDim;
|
||||
const float4 x0 = ld4(xr + swz_lo(tj)), x1 = ld4(xr + swz_hi(tj));
|
||||
const float2 xv[4] = {
|
||||
make_float2(x0.x, x0.y), make_float2(x0.z, x0.w), make_float2(x1.x, x1.y), make_float2(x1.z, x1.w)};
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
const float2 bv = f2(get(b[r], kk));
|
||||
#pragma unroll
|
||||
for (int cp = 0; cp < 4; ++cp)
|
||||
t[r][cp] = fma2(bv, xv[cp], t[r][cp]);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
float* Jn = injection + mat;
|
||||
#pragma unroll
|
||||
for (int r = 0; r < 8; ++r) {
|
||||
float* row = Jn + (i0 + r) * kDim + j0;
|
||||
st4(row, t[r][0], t[r][1]);
|
||||
st4(row + 4, t[r][2], t[r][3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct VdnDeltaFactorsKernel {
|
||||
/**
|
||||
* \brief Validate the tensors and launch one CTA per matrix.
|
||||
*
|
||||
* \param transition [N, 128, 128] fp32 output, diag(alpha) (I + A)^-1
|
||||
* \param injection [N, 128, 128] fp32 output, B (I + A)^-1
|
||||
* \param A [N, 128, 128] fp32 SPD statistics (I + A is inverted)
|
||||
* \param B [N, 128, 128] fp32
|
||||
* \param alpha [N, 128] fp32
|
||||
*/
|
||||
static void
|
||||
run(tvm::ffi::TensorView transition,
|
||||
tvm::ffi::TensorView injection,
|
||||
tvm::ffi::TensorView A,
|
||||
tvm::ffi::TensorView B,
|
||||
tvm::ffi::TensorView alpha) {
|
||||
using namespace host;
|
||||
auto N = SymbolicSize{"num_matrices"};
|
||||
auto device = SymbolicDevice{};
|
||||
device.set_options<kDLCUDA>();
|
||||
TensorMatcher({N, kDim, kDim})
|
||||
.with_dtype<fp32_t>()
|
||||
.with_device(device)
|
||||
.verify(transition)
|
||||
.verify(injection)
|
||||
.verify(A)
|
||||
.verify(B);
|
||||
TensorMatcher({N, kDim}).with_dtype<fp32_t>().with_device(device).verify(alpha);
|
||||
const int64_t num = N.unwrap();
|
||||
if (num == 0) return;
|
||||
CHECK_HOST(
|
||||
transition.data_ptr() != A.data_ptr() && transition.data_ptr() != B.data_ptr() &&
|
||||
injection.data_ptr() != A.data_ptr() && injection.data_ptr() != B.data_ptr() &&
|
||||
transition.data_ptr() != injection.data_ptr())
|
||||
<< "vdn_delta_factors outputs must not alias inputs";
|
||||
// every tensor is read or written as float4; a storage offset breaks this
|
||||
const auto aligned16 = [](const void* p) { return reinterpret_cast<uintptr_t>(p) % 16 == 0; };
|
||||
CHECK_HOST(
|
||||
aligned16(A.data_ptr()) && aligned16(B.data_ptr()) && aligned16(alpha.data_ptr()) &&
|
||||
aligned16(transition.data_ptr()) && aligned16(injection.data_ptr()))
|
||||
<< "vdn_delta_factors needs 16-byte aligned tensors";
|
||||
const DLDevice dev = device.unwrap();
|
||||
// 64 KB of dynamic shared memory needs the opt-in, once per device.
|
||||
static bool attr_set[64] = {};
|
||||
const int dev_id = dev.device_id;
|
||||
CHECK_HOST(dev_id >= 0 && dev_id < 64) << "vdn_delta_factors: unexpected device id " << dev_id;
|
||||
if (!attr_set[dev_id]) {
|
||||
CHECK_CUDA(cudaFuncSetAttribute(vdn_delta_factors_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, kXsBytes))
|
||||
<< "vdn_delta_factors: cannot reserve " << kXsBytes << " bytes of dynamic shared memory";
|
||||
attr_set[dev_id] = true;
|
||||
}
|
||||
LaunchKernel(static_cast<uint32_t>(num), kBlockSize, dev, kXsBytes)(
|
||||
vdn_delta_factors_kernel,
|
||||
static_cast<const float*>(A.data_ptr()),
|
||||
static_cast<const float*>(B.data_ptr()),
|
||||
static_cast<const float*>(alpha.data_ptr()),
|
||||
static_cast<float*>(transition.data_ptr()),
|
||||
static_cast<float*>(injection.data_ptr()));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace vdn_delta_factors
|
||||
|
||||
} // namespace sglang
|
||||
@@ -37,6 +37,7 @@ norm/ RMSNorm / LayerNorm / GroupNorm and their fused epilogues
|
||||
modulate/ adaLN modulate, gating, timestep conditioning
|
||||
rope/ rotary embeddings and the QK-norm chains fused into them
|
||||
activation/ SiLU / GLU / GELU fusions
|
||||
quantization/ MXFP8 producers whose scales land in the GEMM's swizzled layout
|
||||
attention/ sparse linear attention, gated delta-net
|
||||
routing/ diffusion-model MoE routing and expert selection
|
||||
layout/ pure data movement: USP/Ulysses relayout, varlen pack, causal pad
|
||||
@@ -140,6 +141,7 @@ tensor copy per residual site.
|
||||
|---|---|---|
|
||||
| `fused_inplace_qknorm_rope` | JIT CUDA | one bf16 rounding step vs split baseline; `round_norm_before_rope=True` makes it exact; supports compact and full-width NeoX/interleaved caches |
|
||||
| `fused_qknorm_rope_pack_kv` | JIT CUDA | as above, also packs prefix K/V |
|
||||
| `fused_qknorm_rope_out_of_place` | JIT CUDA | as above, bit-equal to the in-place kernel; reads strided q/k and writes contiguous copies, inputs untouched (VDN-H3 keeps the raw q/k for its linear branch) |
|
||||
| `try_fused_flux2_qkv_epilogue` | KDA (JIT CUDA) | bit-exact vs the selected BF16 chain | FLUX.2 QK RMSNorm + RoPE + joint QKV packing |
|
||||
| `try_fused_qwen_qkv_epilogue` | JIT CUDA | bit-exact vs the selected BF16 chain | Qwen-Image QK RMSNorm + RoPE + joint QKV writes; SM100+ |
|
||||
| `fused_rope_rotate_half_bitexact` | Triton | bit-exact (elementwise only) |
|
||||
@@ -150,6 +152,22 @@ tensor copy per residual site.
|
||||
| `apply_rotary_embedding` | Triton (+fallbacks) | close; the generic entry point |
|
||||
| `hunyuan_qkv_rope_pack` | Triton | bit-exact; packs QKV and applies RoPE in one pass |
|
||||
|
||||
### MiniMax-H3 / VDN-H3 linear branch
|
||||
|
||||
| Entry point | Backend | Contract |
|
||||
|---|---|---|
|
||||
| `vdn_frame_stats_prep`, `vdn_gather_linear_state` | Triton | bit-exact (same products, fp32 gather) |
|
||||
| `vdn_temporal_conv_act`, `vdn_silu_l2norm`, `vdn_linear_epilogue` | Triton | one rounding at the store, within one bf16 ulp of the eager chain; the model's own inference kernels, mounted unconditionally by the VDN-H3 branch |
|
||||
| `vdn_delta_factors` | JIT CUDA | `(alpha * inv(I + A), B @ inv(I + A))` in one launch; same fp32 accuracy class as the cholesky + solve_triangular chain (cond-dominated); head_dim 128 |
|
||||
|
||||
### MXFP8 producers (online `mxfp8`, cuBLASLt block-scaled GEMM on SM100)
|
||||
|
||||
| Entry point | Backend | Contract |
|
||||
|---|---|---|
|
||||
| `mxfp8_quantize_swizzled` | Triton | bit-exact vs `flashinfer.mxfp8_quantize(x, True)`: e4m3 payload + block-32 E8M0 scales in the `SWIZZLE_32_4_4` layout; weights at load and any bf16 GEMM input |
|
||||
| `silu_mul_mxfp8` | Triton | bit-exact vs eager bf16 `silu(gate) * up` followed by the quantizer above; the fc2 input |
|
||||
| `indexed_scale_shift_mxfp8_` | Triton | bit-exact vs `indexed_scale_shift_bf16_` followed by the quantizer above, optionally keeping the bf16 rows in place; the qkv / fc1 inputs |
|
||||
|
||||
### MoE routing
|
||||
|
||||
| Entry point | Backend | Contract | Applies to |
|
||||
|
||||
@@ -328,6 +328,76 @@ _SPECS: tuple[tuple[str, KernelBackend, str, frozenset, str], ...] = (
|
||||
_CUDA,
|
||||
"Sana-WM bidirectional gated delta-net.",
|
||||
),
|
||||
(
|
||||
"diffusion.fused_qknorm_rope_out_of_place",
|
||||
KernelBackend.JIT,
|
||||
"rope.qknorm_rope_jit:fused_qknorm_rope_out_of_place",
|
||||
_CUDA,
|
||||
"Out-of-place fused QK-norm + RoPE (raw q/k preserved).",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_delta_factors",
|
||||
KernelBackend.JIT,
|
||||
"attention.vdn_delta_factors_jit:vdn_delta_factors",
|
||||
_CUDA,
|
||||
"VDN-H3 delta rule: fused (I + A)^-1 -> transition / injection (fp32, head_dim 128).",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_temporal_conv_act",
|
||||
KernelBackend.TRITON,
|
||||
"attention.vdn_linear_branch_triton:vdn_temporal_conv_act",
|
||||
_CUDA,
|
||||
"VDN-H3 linear branch: 5-tap temporal conv + SiLU + L2 norm.",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_silu_l2norm",
|
||||
KernelBackend.TRITON,
|
||||
"attention.vdn_linear_branch_triton:vdn_silu_l2norm",
|
||||
_CUDA,
|
||||
"VDN-H3 linear branch: SiLU + L2 norm over head_dim.",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_frame_stats_prep",
|
||||
KernelBackend.TRITON,
|
||||
"attention.vdn_linear_branch_triton:vdn_frame_stats_prep",
|
||||
_CUDA,
|
||||
"VDN-H3 linear branch: frame-statistics GEMM operands in one pass.",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_gather_linear_state",
|
||||
KernelBackend.TRITON,
|
||||
"attention.vdn_linear_branch_triton:vdn_gather_linear_state",
|
||||
_CUDA,
|
||||
"VDN-H3 linear branch: alpha-bridged boundary gather in one pass.",
|
||||
),
|
||||
(
|
||||
"diffusion.vdn_linear_epilogue",
|
||||
KernelBackend.TRITON,
|
||||
"attention.vdn_linear_branch_triton:vdn_linear_epilogue",
|
||||
_CUDA,
|
||||
"VDN-H3 linear branch: RMSNorm * gate readout epilogue.",
|
||||
),
|
||||
(
|
||||
"diffusion.mxfp8_quantize_swizzled",
|
||||
KernelBackend.TRITON,
|
||||
"quantization.mxfp8_swizzled_triton:mxfp8_quantize_swizzled",
|
||||
_CUDA,
|
||||
"bf16 -> MXFP8 (e4m3, block-32 E8M0 scales in the cuBLASLt swizzled layout).",
|
||||
),
|
||||
(
|
||||
"diffusion.silu_mul_mxfp8",
|
||||
KernelBackend.TRITON,
|
||||
"quantization.mxfp8_swizzled_triton:silu_mul_mxfp8",
|
||||
_CUDA,
|
||||
"SwiGLU + MXFP8 quant for the online mxfp8 fc2 input.",
|
||||
),
|
||||
(
|
||||
"diffusion.indexed_scale_shift_mxfp8_",
|
||||
KernelBackend.TRITON,
|
||||
"quantization.mxfp8_swizzled_triton:indexed_scale_shift_mxfp8_",
|
||||
_CUDA,
|
||||
"Indexed adaLN modulation + MXFP8 quant for the online mxfp8 qkv/fc1 inputs.",
|
||||
),
|
||||
(
|
||||
"diffusion.group_limited_topk",
|
||||
KernelBackend.TRITON,
|
||||
@@ -541,6 +611,24 @@ _EXPORTS: dict[str, str] = {
|
||||
"fused_causal_conv3d_cat_pad_cuda": "sglang.kernels.kda_kernels.causal_conv3d_cat_pad_jit",
|
||||
"fused_causal_conv3d_cat_pad": "layout.causal_conv3d_cat_pad_triton",
|
||||
"pack_qkv_destination_major": "layout.ulysses_qkv_triton",
|
||||
"fused_qknorm_rope_out_of_place": "rope.qknorm_rope_jit",
|
||||
"vdn_delta_factors": "attention.vdn_delta_factors_jit",
|
||||
"can_use_vdn_delta_factors": "attention.vdn_delta_factors_jit",
|
||||
"vdn_temporal_conv_act": "attention.vdn_linear_branch_triton",
|
||||
"can_use_vdn_temporal_conv_act": "attention.vdn_linear_branch_triton",
|
||||
"can_use_vdn_silu_l2norm": "attention.vdn_linear_branch_triton",
|
||||
"can_use_vdn_frame_stats_prep": "attention.vdn_linear_branch_triton",
|
||||
"can_use_vdn_gather_linear_state": "attention.vdn_linear_branch_triton",
|
||||
"can_use_vdn_linear_epilogue": "attention.vdn_linear_branch_triton",
|
||||
"vdn_silu_l2norm": "attention.vdn_linear_branch_triton",
|
||||
"vdn_frame_stats_prep": "attention.vdn_linear_branch_triton",
|
||||
"vdn_gather_linear_state": "attention.vdn_linear_branch_triton",
|
||||
"vdn_linear_epilogue": "attention.vdn_linear_branch_triton",
|
||||
"can_use_mxfp8_swizzled": "quantization.mxfp8_swizzled_triton",
|
||||
"can_use_silu_mul_mxfp8": "quantization.mxfp8_swizzled_triton",
|
||||
"indexed_scale_shift_mxfp8_": "quantization.mxfp8_swizzled_triton",
|
||||
"mxfp8_quantize_swizzled": "quantization.mxfp8_swizzled_triton",
|
||||
"silu_mul_mxfp8": "quantization.mxfp8_swizzled_triton",
|
||||
"can_use_usp_merge_heads": "layout.usp_relayout_jit",
|
||||
"usp_merge_heads": "layout.usp_relayout_jit",
|
||||
"build_inv_indices": "layout.varlen_pack_pad_triton",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Fused VDN-H3 delta-rule factors: (I + A)^-1 folded into the transition and injection.
|
||||
|
||||
One CUDA kernel (block Gauss-Jordan inverse in registers + the two products) replaces the
|
||||
cholesky / solve_triangular / GEMM chain of ``delta_factor_apply`` for the ``vdn_solve`` and
|
||||
``vdn_scaled`` rules. fp32, head_dim 128 only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.jit.utils import cache_once, load_jit
|
||||
from sglang.srt.utils.custom_op import register_custom_op
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
HEAD_DIM = 128
|
||||
_FLOAT4_BYTES = 16
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_vdn_delta_factors_module() -> Module:
|
||||
if torch.cuda.get_device_capability()[0] < 8:
|
||||
raise RuntimeError(
|
||||
"vdn_delta_factors needs SM80 or later (2 x 70 KB shared memory per SM)"
|
||||
)
|
||||
return load_jit(
|
||||
"diffusion_vdn_delta_factors",
|
||||
cuda_files=["diffusion/vdn_delta_factors.cuh"],
|
||||
cuda_wrappers=[
|
||||
("vdn_delta_factors", "vdn_delta_factors::VdnDeltaFactorsKernel::run")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _aligned(t: torch.Tensor) -> torch.Tensor:
|
||||
# the kernel loads float4; .contiguous() keeps a storage offset, a fresh allocation is aligned
|
||||
return t if t.data_ptr() % _FLOAT4_BYTES == 0 else t.clone()
|
||||
|
||||
|
||||
def _fake_impl(
|
||||
A: torch.Tensor, B: torch.Tensor, alpha: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
del alpha
|
||||
return torch.empty_like(A), torch.empty_like(B)
|
||||
|
||||
|
||||
@register_custom_op(
|
||||
op_name="diffusion_vdn_delta_factors",
|
||||
mutates_args=[],
|
||||
fake_impl=_fake_impl,
|
||||
)
|
||||
def vdn_delta_factors(
|
||||
A: torch.Tensor, B: torch.Tensor, alpha: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""``(alpha[..., :, None] * inv(I + A), B @ inv(I + A))`` for fp32 ``[..., 128, 128]`` SPD ``A``.
|
||||
|
||||
``B`` has the shape of ``A``; ``alpha`` is ``[..., 128]``. Same accuracy as the eager
|
||||
cholesky path (both are dominated by cond(I + A) in fp32).
|
||||
"""
|
||||
A, B, alpha = _aligned(A), _aligned(B), _aligned(alpha)
|
||||
transition = torch.empty_like(A)
|
||||
injection = torch.empty_like(B)
|
||||
module = _jit_vdn_delta_factors_module()
|
||||
module.vdn_delta_factors(
|
||||
transition.view(-1, HEAD_DIM, HEAD_DIM),
|
||||
injection.view(-1, HEAD_DIM, HEAD_DIM),
|
||||
A.view(-1, HEAD_DIM, HEAD_DIM),
|
||||
B.view(-1, HEAD_DIM, HEAD_DIM),
|
||||
alpha.view(-1, HEAD_DIM),
|
||||
)
|
||||
return transition, injection
|
||||
|
||||
|
||||
def can_use_vdn_delta_factors(
|
||||
A: torch.Tensor, B: torch.Tensor, alpha: torch.Tensor
|
||||
) -> bool:
|
||||
return (
|
||||
A.is_cuda
|
||||
and A.dtype is torch.float32
|
||||
and B.dtype is torch.float32
|
||||
and alpha.dtype is torch.float32
|
||||
and A.device == B.device == alpha.device
|
||||
and A.dim() >= 2
|
||||
and A.shape[-1] == HEAD_DIM
|
||||
and A.shape[-2] == HEAD_DIM
|
||||
and B.shape == A.shape
|
||||
and alpha.shape == A.shape[:-1]
|
||||
and A.is_contiguous()
|
||||
and B.is_contiguous()
|
||||
and alpha.is_contiguous()
|
||||
and torch.cuda.get_device_capability(A.device)[0] >= 8
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"can_use_vdn_delta_factors",
|
||||
"vdn_delta_factors",
|
||||
]
|
||||
@@ -0,0 +1,569 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fused Triton kernels for the VDN-H3 (Video DeltaNet MiniMax-H3) linear branch;
|
||||
each reads its operands once and rounds once at the store.
|
||||
|
||||
vdn_temporal_conv_act 5-tap depthwise temporal conv + SiLU [+ L2 norm]
|
||||
(port of OpenVDN's _tconv_act_kernel)
|
||||
vdn_silu_l2norm SiLU [+ L2 norm] over head_dim, strided input ok
|
||||
vdn_frame_stats_prep the four GEMM operands of the frame statistics
|
||||
(kf16, kf32, kf32 * beta, v * beta) in [F, H, S, d]
|
||||
off one read of k and one of v
|
||||
vdn_gather_linear_state the alpha-bridged boundary gather over the fp32
|
||||
state banks
|
||||
vdn_linear_epilogue RMSNorm(d) * gate with the [F, H, S, d] -> [F*S, H*d]
|
||||
transpose folded into the store
|
||||
|
||||
Contract: vdn_frame_stats_prep and vdn_gather_linear_state are bitwise equal
|
||||
to the eager chains (widening casts, same products, fp32 gather). The three
|
||||
activation kernels round once instead of once per op and sit within one bf16
|
||||
ulp of the eager bf16 chains; OpenVDN ships the same contract, so the branch
|
||||
mounts them unconditionally.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
_BLOCK_T = 16
|
||||
_BLOCK_ROWS = 32
|
||||
|
||||
|
||||
def _pow2_head_dim(head_dim: int) -> bool:
|
||||
return head_dim >= 16 and head_dim & (head_dim - 1) == 0
|
||||
|
||||
|
||||
def _check_head_dim(head_dim: int) -> None:
|
||||
if not _pow2_head_dim(head_dim):
|
||||
raise ValueError(
|
||||
f"head_dim must be a power of two >= 16 (tl.arange), got {head_dim}"
|
||||
)
|
||||
|
||||
|
||||
def _cuda_bf16_rows(t: torch.Tensor) -> bool:
|
||||
return t.is_cuda and t.dtype == torch.bfloat16 and t.stride(-1) == 1
|
||||
|
||||
|
||||
def _i32(t: torch.Tensor) -> torch.Tensor:
|
||||
return t.to(torch.int32).contiguous()
|
||||
|
||||
|
||||
def can_use_vdn_temporal_conv_act(x: torch.Tensor, heads: int, head_dim: int) -> bool:
|
||||
"""x [T, S, heads * head_dim] bf16 on CUDA, power-of-two head_dim."""
|
||||
return (
|
||||
_cuda_bf16_rows(x)
|
||||
and x.ndim == 3
|
||||
and x.shape[-1] == heads * head_dim
|
||||
and _pow2_head_dim(head_dim)
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
def can_use_vdn_silu_l2norm(tokens: torch.Tensor) -> bool:
|
||||
"""tokens [N, H, d] bf16 on CUDA (any row/head strides), power-of-two d."""
|
||||
return (
|
||||
_cuda_bf16_rows(tokens)
|
||||
and tokens.ndim == 3
|
||||
and _pow2_head_dim(tokens.shape[-1])
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
def can_use_vdn_frame_stats_prep(key: torch.Tensor, value: torch.Tensor) -> bool:
|
||||
"""key/value [F * S, H, d] bf16 on CUDA with matching shapes."""
|
||||
return (
|
||||
_cuda_bf16_rows(key)
|
||||
and _cuda_bf16_rows(value)
|
||||
and key.ndim == 3
|
||||
and key.shape == value.shape
|
||||
and _pow2_head_dim(key.shape[-1])
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
def can_use_vdn_gather_linear_state(prefix: torch.Tensor) -> bool:
|
||||
"""prefix/suffix [F, H, dv, dk] fp32 on CUDA, power-of-two dk."""
|
||||
return (
|
||||
prefix.is_cuda
|
||||
and prefix.dtype == torch.float32
|
||||
and prefix.ndim == 4
|
||||
and _pow2_head_dim(prefix.shape[-1])
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
def can_use_vdn_linear_epilogue(readout: torch.Tensor) -> bool:
|
||||
"""readout [F, H, S, d] bf16 on CUDA, power-of-two d."""
|
||||
return (
|
||||
_cuda_bf16_rows(readout)
|
||||
and readout.ndim == 4
|
||||
and _pow2_head_dim(readout.shape[-1])
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# temporal conv + SiLU + L2 norm
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _tconv_act_kernel(
|
||||
X,
|
||||
W,
|
||||
OUT,
|
||||
num_frames,
|
||||
tokens_per_frame,
|
||||
channels,
|
||||
BLOCK_T: tl.constexpr,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
L2NORM: tl.constexpr,
|
||||
HEADS: tl.constexpr,
|
||||
FRAME_MAJOR: tl.constexpr,
|
||||
):
|
||||
pid_t = tl.program_id(0)
|
||||
pid_s = tl.program_id(1)
|
||||
pid_h = tl.program_id(2)
|
||||
chan = pid_h * HEAD_DIM + tl.arange(0, HEAD_DIM)
|
||||
rows = pid_t * BLOCK_T + tl.arange(0, BLOCK_T)
|
||||
valid = rows < num_frames
|
||||
|
||||
acc = tl.zeros((BLOCK_T, HEAD_DIM), dtype=tl.float32)
|
||||
for dt in tl.static_range(5):
|
||||
r = rows + dt - 2
|
||||
ok = valid & (r >= 0) & (r < num_frames) # zero padding, both ends
|
||||
v = tl.load(
|
||||
X
|
||||
+ (r[:, None].to(tl.int64) * tokens_per_frame + pid_s) * channels
|
||||
+ chan[None, :],
|
||||
mask=ok[:, None],
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
wd = tl.load(W + chan * 5 + dt).to(tl.float32)
|
||||
acc += v * wd[None, :]
|
||||
|
||||
y = acc * tl.sigmoid(acc) # SiLU
|
||||
if L2NORM:
|
||||
inv = 1.0 / tl.sqrt(tl.maximum(tl.sum(y * y, axis=1), 1e-12))
|
||||
y = y * inv[:, None]
|
||||
if FRAME_MAJOR:
|
||||
# [T, HEADS, S, D]: the readout bmm reads this layout directly
|
||||
dst = (
|
||||
(rows[:, None].to(tl.int64) * HEADS + pid_h) * tokens_per_frame + pid_s
|
||||
) * HEAD_DIM + tl.arange(0, HEAD_DIM)[None, :]
|
||||
else:
|
||||
dst = (rows[:, None].to(tl.int64) * tokens_per_frame + pid_s) * channels + chan[
|
||||
None, :
|
||||
]
|
||||
tl.store(OUT + dst, y.to(OUT.dtype.element_ty), mask=valid[:, None])
|
||||
|
||||
|
||||
def vdn_temporal_conv_act(
|
||||
x: torch.Tensor,
|
||||
w: torch.Tensor,
|
||||
heads: int,
|
||||
head_dim: int,
|
||||
l2norm: bool,
|
||||
frame_major: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""x [T, S, C] bf16 contiguous, w [C, 5] -> [T * S, heads, head_dim], or
|
||||
[T, heads, S, head_dim] with ``frame_major``."""
|
||||
if not x.is_cuda:
|
||||
raise ValueError("vdn_temporal_conv_act is a Triton kernel; x must be on CUDA")
|
||||
_check_head_dim(head_dim)
|
||||
num_frames, tokens_per_frame, channels = x.shape
|
||||
if channels != heads * head_dim:
|
||||
raise ValueError(f"C={channels} != heads*head_dim={heads * head_dim}")
|
||||
if w.shape != (channels, 5):
|
||||
raise ValueError(f"w must be [C, 5], got {tuple(w.shape)}")
|
||||
x = x.contiguous()
|
||||
w = w.contiguous()
|
||||
out = torch.empty_like(x)
|
||||
_tconv_act_kernel[(triton.cdiv(num_frames, _BLOCK_T), tokens_per_frame, heads)](
|
||||
x,
|
||||
w,
|
||||
out,
|
||||
num_frames,
|
||||
tokens_per_frame,
|
||||
channels,
|
||||
BLOCK_T=_BLOCK_T,
|
||||
HEAD_DIM=head_dim,
|
||||
L2NORM=l2norm,
|
||||
HEADS=heads,
|
||||
FRAME_MAJOR=frame_major,
|
||||
num_warps=4,
|
||||
num_stages=2,
|
||||
)
|
||||
if frame_major:
|
||||
return out.view(num_frames, heads, tokens_per_frame, head_dim)
|
||||
return out.view(num_frames * tokens_per_frame, heads, head_dim)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# SiLU + L2 norm on a (possibly strided) [N, H, d] tensor
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _silu_l2norm_kernel(
|
||||
X,
|
||||
OUT,
|
||||
N,
|
||||
stride_n,
|
||||
stride_h,
|
||||
H,
|
||||
tokens_per_frame,
|
||||
BLOCK_N: tl.constexpr,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
L2NORM: tl.constexpr,
|
||||
FRAME_MAJOR: tl.constexpr,
|
||||
):
|
||||
pid_n = tl.program_id(0)
|
||||
pid_h = tl.program_id(1)
|
||||
rows = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
||||
valid = rows < N
|
||||
offs = tl.arange(0, HEAD_DIM)
|
||||
x = tl.load(
|
||||
X + rows[:, None].to(tl.int64) * stride_n + pid_h * stride_h + offs[None, :],
|
||||
mask=valid[:, None],
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
y = x * tl.sigmoid(x)
|
||||
if L2NORM:
|
||||
inv = 1.0 / tl.sqrt(tl.maximum(tl.sum(y * y, axis=1), 1e-12))
|
||||
y = y * inv[:, None]
|
||||
if FRAME_MAJOR:
|
||||
# row n = frame * S_ + s -> [F, H, S_, D]
|
||||
frame = rows // tokens_per_frame
|
||||
pos = rows - frame * tokens_per_frame
|
||||
dst = (
|
||||
(frame[:, None].to(tl.int64) * H + pid_h) * tokens_per_frame + pos[:, None]
|
||||
) * HEAD_DIM + offs[None, :]
|
||||
else:
|
||||
dst = (rows[:, None].to(tl.int64) * H + pid_h) * HEAD_DIM + offs[None, :]
|
||||
tl.store(OUT + dst, y.to(OUT.dtype.element_ty), mask=valid[:, None])
|
||||
|
||||
|
||||
def vdn_silu_l2norm(
|
||||
tokens: torch.Tensor, l2norm: bool, per_frame: int | None = None
|
||||
) -> torch.Tensor:
|
||||
"""tokens [N, H, d] (last dim contiguous) -> contiguous [N, H, d], or
|
||||
[N / per_frame, H, per_frame, d] when ``per_frame`` is given."""
|
||||
if not tokens.is_cuda:
|
||||
raise ValueError("vdn_silu_l2norm is a Triton kernel; tokens must be on CUDA")
|
||||
N, H, D = tokens.shape
|
||||
_check_head_dim(D)
|
||||
if tokens.stride(-1) != 1:
|
||||
tokens = tokens.contiguous()
|
||||
frame_major = per_frame is not None
|
||||
if frame_major and (per_frame <= 0 or N % per_frame):
|
||||
raise ValueError(f"per_frame={per_frame} must divide N={N}")
|
||||
shape = (N // per_frame, H, per_frame, D) if frame_major else (N, H, D)
|
||||
out = torch.empty(shape, dtype=tokens.dtype, device=tokens.device)
|
||||
if N == 0:
|
||||
return out
|
||||
_silu_l2norm_kernel[(triton.cdiv(N, _BLOCK_ROWS), H)](
|
||||
tokens,
|
||||
out,
|
||||
N,
|
||||
tokens.stride(0),
|
||||
tokens.stride(1),
|
||||
H,
|
||||
per_frame if frame_major else 1,
|
||||
BLOCK_N=_BLOCK_ROWS,
|
||||
HEAD_DIM=D,
|
||||
L2NORM=l2norm,
|
||||
FRAME_MAJOR=frame_major,
|
||||
num_warps=4,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# frame statistics prologue
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _frame_stats_prep_kernel(
|
||||
K,
|
||||
V,
|
||||
BETA,
|
||||
K16,
|
||||
K32,
|
||||
KB32,
|
||||
VB,
|
||||
tokens_per_frame,
|
||||
H,
|
||||
BLOCK_S: tl.constexpr,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
):
|
||||
pid_s = tl.program_id(0)
|
||||
f = tl.program_id(1)
|
||||
h = tl.program_id(2)
|
||||
s = pid_s * BLOCK_S + tl.arange(0, BLOCK_S)
|
||||
valid = s < tokens_per_frame
|
||||
offs = tl.arange(0, HEAD_DIM)
|
||||
rows = f * tokens_per_frame + s # token rows
|
||||
src = (rows[:, None].to(tl.int64) * H + h) * HEAD_DIM + offs[None, :] # [F*S, H, d]
|
||||
dst = ((f * H + h) * tokens_per_frame + s)[:, None] * HEAD_DIM + offs[
|
||||
None, :
|
||||
] # [F, H, S, d]
|
||||
k = tl.load(K + src, mask=valid[:, None], other=0.0)
|
||||
v = tl.load(V + src, mask=valid[:, None], other=0.0)
|
||||
beta = tl.load(BETA + rows * H + h, mask=valid, other=0.0)
|
||||
k32 = k.to(tl.float32)
|
||||
beta32 = beta.to(tl.float32)
|
||||
tl.store(K16 + dst, k, mask=valid[:, None])
|
||||
tl.store(K32 + dst, k32, mask=valid[:, None])
|
||||
tl.store(KB32 + dst, k32 * beta32[:, None], mask=valid[:, None])
|
||||
vb = (v.to(tl.float32) * beta32[:, None]).to(VB.dtype.element_ty)
|
||||
tl.store(VB + dst, vb, mask=valid[:, None])
|
||||
|
||||
|
||||
def vdn_frame_stats_prep(
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
beta: torch.Tensor,
|
||||
num_frames: int,
|
||||
tokens_per_frame: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""key/value [F*S, H, d] bf16 contiguous, beta [F*S, H] bf16 ->
|
||||
(k16 [F,H,S,d] bf16, k32 fp32, k32*beta fp32, v*beta bf16), all contiguous."""
|
||||
if not key.is_cuda:
|
||||
raise ValueError(
|
||||
"vdn_frame_stats_prep is a Triton kernel; inputs must be on CUDA"
|
||||
)
|
||||
rows, H, D = key.shape
|
||||
_check_head_dim(D)
|
||||
if rows != num_frames * tokens_per_frame:
|
||||
raise ValueError(f"{rows} rows != {num_frames} x {tokens_per_frame}")
|
||||
key = key.contiguous()
|
||||
value = value.contiguous()
|
||||
beta = beta.to(key.dtype).contiguous()
|
||||
shape = (num_frames, H, tokens_per_frame, D)
|
||||
k16 = torch.empty(shape, dtype=key.dtype, device=key.device)
|
||||
k32 = torch.empty(shape, dtype=torch.float32, device=key.device)
|
||||
kb32 = torch.empty(shape, dtype=torch.float32, device=key.device)
|
||||
vb = torch.empty(shape, dtype=value.dtype, device=key.device)
|
||||
_frame_stats_prep_kernel[
|
||||
(triton.cdiv(tokens_per_frame, _BLOCK_ROWS), num_frames, H)
|
||||
](
|
||||
key,
|
||||
value,
|
||||
beta,
|
||||
k16,
|
||||
k32,
|
||||
kb32,
|
||||
vb,
|
||||
tokens_per_frame,
|
||||
H,
|
||||
BLOCK_S=_BLOCK_ROWS,
|
||||
HEAD_DIM=D,
|
||||
num_warps=4,
|
||||
)
|
||||
return k16, k32, kb32, vb
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# readout epilogue: RMSNorm(d) * gate, [F, H, S, d] -> [F*S, H*d]
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _linear_epilogue_kernel(
|
||||
R,
|
||||
W,
|
||||
G,
|
||||
OUT,
|
||||
tokens_per_frame,
|
||||
H,
|
||||
eps,
|
||||
BLOCK_S: tl.constexpr,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
):
|
||||
pid_s = tl.program_id(0)
|
||||
f = tl.program_id(1)
|
||||
h = tl.program_id(2)
|
||||
s = pid_s * BLOCK_S + tl.arange(0, BLOCK_S)
|
||||
valid = s < tokens_per_frame
|
||||
offs = tl.arange(0, HEAD_DIM)
|
||||
src = ((f * H + h) * tokens_per_frame + s)[:, None] * HEAD_DIM + offs[None, :]
|
||||
rows = f * tokens_per_frame + s
|
||||
dst = (rows[:, None].to(tl.int64) * H + h) * HEAD_DIM + offs[None, :]
|
||||
r = tl.load(R + src, mask=valid[:, None], other=0.0).to(tl.float32)
|
||||
ms = tl.sum(r * r, axis=1) / HEAD_DIM
|
||||
w = tl.load(W + offs).to(tl.float32)
|
||||
g = tl.load(G + dst, mask=valid[:, None], other=0.0).to(tl.float32)
|
||||
y = r * (1.0 / tl.sqrt(ms + eps))[:, None] * w[None, :] * g
|
||||
tl.store(OUT + dst, y.to(OUT.dtype.element_ty), mask=valid[:, None])
|
||||
|
||||
|
||||
def vdn_linear_epilogue(
|
||||
readout: torch.Tensor,
|
||||
norm_weight: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""readout [F, H, S, d] bf16 contiguous, norm_weight [d], gate [F*S, H, d]
|
||||
-> [F*S, H*d] bf16."""
|
||||
if not readout.is_cuda:
|
||||
raise ValueError(
|
||||
"vdn_linear_epilogue is a Triton kernel; readout must be on CUDA"
|
||||
)
|
||||
F, H, tokens_per_frame, D = readout.shape
|
||||
_check_head_dim(D)
|
||||
readout = readout.contiguous()
|
||||
gate = gate.reshape(F * tokens_per_frame, H, D).to(readout.dtype).contiguous()
|
||||
out = torch.empty(
|
||||
(F * tokens_per_frame, H * D), dtype=readout.dtype, device=readout.device
|
||||
)
|
||||
_linear_epilogue_kernel[(triton.cdiv(tokens_per_frame, _BLOCK_ROWS), F, H)](
|
||||
readout,
|
||||
norm_weight.contiguous(),
|
||||
gate,
|
||||
out,
|
||||
tokens_per_frame,
|
||||
H,
|
||||
float(eps),
|
||||
BLOCK_S=_BLOCK_ROWS,
|
||||
HEAD_DIM=D,
|
||||
num_warps=4,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# boundary gather: prefix[lo-1] * prod alpha + suffix[hi+1] * prod alpha
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _gather_state_kernel(
|
||||
PREFIX,
|
||||
SUFFIX,
|
||||
LOGP, # [F+1, H, dk] fp32 exclusive log-alpha prefix sums
|
||||
TEXT, # [H, dv, dk] fp32 (or PREFIX when HAS_TEXT is False)
|
||||
BEFORE, # [F] int32 prefix row to read (clamped)
|
||||
AFTER, # [F] int32 suffix row to read (clamped)
|
||||
HASB, # [F] int32 0/1
|
||||
HASA, # [F] int32 0/1
|
||||
BRIDGEB, # [F] int32 log-prefix row for the before side
|
||||
BRIDGEA, # [F] int32 log-prefix row for the after side
|
||||
OUT,
|
||||
H,
|
||||
DV,
|
||||
HAS_TEXT: tl.constexpr,
|
||||
BRIDGE: tl.constexpr,
|
||||
BLOCK_V: tl.constexpr,
|
||||
DK: tl.constexpr,
|
||||
):
|
||||
f = tl.program_id(0)
|
||||
h = tl.program_id(1)
|
||||
pid_v = tl.program_id(2)
|
||||
rows = pid_v * BLOCK_V + tl.arange(0, BLOCK_V)
|
||||
cols = tl.arange(0, DK)
|
||||
valid = rows < DV
|
||||
fb = tl.load(BEFORE + f)
|
||||
fa = tl.load(AFTER + f)
|
||||
has_b = tl.load(HASB + f)
|
||||
has_a = tl.load(HASA + f)
|
||||
plane = rows[:, None] * DK + cols[None, :]
|
||||
off_b = ((fb * H + h) * DV) * DK + plane
|
||||
off_a = ((fa * H + h) * DV) * DK + plane
|
||||
sb = tl.load(PREFIX + off_b, mask=valid[:, None], other=0.0)
|
||||
sa = tl.load(SUFFIX + off_a, mask=valid[:, None], other=0.0)
|
||||
if HAS_TEXT:
|
||||
ts = tl.load(TEXT + (h * DV) * DK + plane, mask=valid[:, None], other=0.0)
|
||||
sb = tl.where(has_b != 0, sb, ts)
|
||||
sa = tl.where(has_a != 0, sa, ts)
|
||||
else:
|
||||
sb = tl.where(has_b != 0, sb, 0.0)
|
||||
sa = tl.where(has_a != 0, sa, 0.0)
|
||||
if BRIDGE:
|
||||
bb = tl.load(BRIDGEB + f)
|
||||
ba = tl.load(BRIDGEA + f)
|
||||
lp_t1 = tl.load(LOGP + ((f + 1) * H + h) * DK + cols)
|
||||
lp_t = tl.load(LOGP + (f * H + h) * DK + cols)
|
||||
lp_bb = tl.load(LOGP + (bb * H + h) * DK + cols)
|
||||
lp_ba = tl.load(LOGP + (ba * H + h) * DK + cols)
|
||||
sb = sb * tl.exp(lp_t1 - lp_bb)[None, :]
|
||||
sa = sa * tl.exp(lp_ba - lp_t)[None, :]
|
||||
out = sb + sa
|
||||
tl.store(
|
||||
OUT + ((f * H + h) * DV) * DK + plane,
|
||||
out.to(OUT.dtype.element_ty),
|
||||
mask=valid[:, None],
|
||||
)
|
||||
|
||||
|
||||
def vdn_gather_linear_state(
|
||||
prefix: torch.Tensor,
|
||||
suffix: torch.Tensor,
|
||||
alpha: torch.Tensor,
|
||||
text_state: torch.Tensor | None,
|
||||
*,
|
||||
before_idx: torch.Tensor,
|
||||
after_idx: torch.Tensor,
|
||||
has_before: torch.Tensor,
|
||||
has_after: torch.Tensor,
|
||||
bridge_before: torch.Tensor,
|
||||
bridge_after: torch.Tensor,
|
||||
bridge: bool,
|
||||
out_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
"""The boundary gather of the linear branch as one kernel over the
|
||||
[F, H, dv, dk] fp32 state banks."""
|
||||
if not prefix.is_cuda:
|
||||
raise ValueError(
|
||||
"vdn_gather_linear_state is a Triton kernel; inputs must be on CUDA"
|
||||
)
|
||||
F, H, DV, DK = prefix.shape
|
||||
_check_head_dim(DK)
|
||||
prefix = prefix.contiguous()
|
||||
suffix = suffix.contiguous()
|
||||
if bridge:
|
||||
log_alpha = torch.log(alpha.float().clamp_min(1e-12))
|
||||
logp = torch.cat(
|
||||
[torch.zeros_like(log_alpha[:1]), log_alpha.cumsum(0)]
|
||||
).contiguous()
|
||||
else:
|
||||
logp = prefix # unused
|
||||
text = text_state.float().contiguous() if text_state is not None else prefix
|
||||
out = torch.empty(prefix.shape, dtype=out_dtype, device=prefix.device)
|
||||
_gather_state_kernel[(F, H, triton.cdiv(DV, _BLOCK_ROWS))](
|
||||
prefix,
|
||||
suffix,
|
||||
logp,
|
||||
text,
|
||||
_i32(before_idx),
|
||||
_i32(after_idx),
|
||||
_i32(has_before),
|
||||
_i32(has_after),
|
||||
_i32(bridge_before),
|
||||
_i32(bridge_after),
|
||||
out,
|
||||
H,
|
||||
DV,
|
||||
HAS_TEXT=text_state is not None,
|
||||
BRIDGE=bridge,
|
||||
BLOCK_V=_BLOCK_ROWS,
|
||||
DK=DK,
|
||||
num_warps=4,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
__all__ = [
|
||||
"can_use_vdn_frame_stats_prep",
|
||||
"can_use_vdn_gather_linear_state",
|
||||
"can_use_vdn_linear_epilogue",
|
||||
"can_use_vdn_silu_l2norm",
|
||||
"can_use_vdn_temporal_conv_act",
|
||||
"vdn_frame_stats_prep",
|
||||
"vdn_gather_linear_state",
|
||||
"vdn_linear_epilogue",
|
||||
"vdn_silu_l2norm",
|
||||
"vdn_temporal_conv_act",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
"""Block-scaled (MXFP8) activation quantizers with GEMM-ready scale layouts."""
|
||||
@@ -0,0 +1,318 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MXFP8 producers: e4m3 payload plus one E8M0 scale per 32 elements along K,
|
||||
the scales in the cuBLASLt ``SWIZZLE_32_4_4`` layout that
|
||||
``torch.nn.functional.scaled_mm(..., BlockWise1x32)`` consumes on SM100.
|
||||
|
||||
Scale ``(r, c)`` of the ``[rows, K/32]`` scale matrix lives at byte
|
||||
``((r // 128) * ceil(K/32 / 4) + c // 4) * 512 + (r % 32) * 16 + ((r % 128) // 32) * 4 + c % 4``,
|
||||
rows padded to 128 and scale columns to 4, padding zero. Exponent
|
||||
``e = ceil(log2(amax / 448))`` exactly from the float bits; ``q = e4m3(x * 2**-e)``;
|
||||
scale byte ``e + 127``. Every producer quantizes the bf16-rounded value the
|
||||
unfused bf16 kernel stores, so each is byte-exact against that kernel followed
|
||||
by ``mxfp8_quantize_swizzled`` (itself byte-exact vs ``flashinfer.mxfp8_quantize``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.kernels.ops.diffusion.common.numerics import round_bf16_to_fp32
|
||||
|
||||
_E4M3 = torch.float8_e4m3fn
|
||||
|
||||
|
||||
def _scale_numel(rows: int, k: int) -> int:
|
||||
n_groups = k // 32
|
||||
return -(-rows // 128) * 128 * (-(-n_groups // 4) * 4)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mx_e8m0_from_amax(amax):
|
||||
bits = amax.to(tl.int32, bitcast=True)
|
||||
e0 = ((bits >> 23) & 0xFF) - 135
|
||||
# amax / 2**e0 lies in [256, 512); bump e0 when it exceeds 448 = 1.75 * 2**8
|
||||
thr = (((e0 + 135) << 23) | 0x600000).to(tl.float32, bitcast=True)
|
||||
e = e0 + (amax > thr).to(tl.int32)
|
||||
e = tl.maximum(e, -127)
|
||||
inv = ((127 - e) << 23).to(tl.float32, bitcast=True)
|
||||
return e + 127, inv
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mx_scale_offsets(r, c, n_col_blocks):
|
||||
tile = (r // 128) * n_col_blocks + (c // 4)
|
||||
return tile * 512 + (r % 32) * 16 + ((r % 128) // 32) * 4 + (c % 4)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mxfp8_quant_kernel(
|
||||
x_ptr,
|
||||
q_ptr,
|
||||
s_ptr,
|
||||
rows,
|
||||
k,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
stride_x,
|
||||
BLOCK_R: tl.constexpr,
|
||||
G: tl.constexpr,
|
||||
):
|
||||
pid_r = tl.program_id(0)
|
||||
pid_g = tl.program_id(1)
|
||||
r = pid_r * BLOCK_R + tl.arange(0, BLOCK_R)
|
||||
g = pid_g * G + tl.arange(0, G)
|
||||
c = pid_g * (G * 32) + tl.arange(0, G * 32)
|
||||
rmask = r < rows
|
||||
mask = rmask[:, None] & (c < k)[None, :]
|
||||
x = tl.load(
|
||||
x_ptr + r[:, None].to(tl.int64) * stride_x + c[None, :], mask=mask, other=0.0
|
||||
).to(tl.float32)
|
||||
x3 = tl.reshape(x, [BLOCK_R, G, 32])
|
||||
amax = tl.max(tl.abs(x3), axis=2)
|
||||
sbyte, inv = _mx_e8m0_from_amax(amax)
|
||||
q = tl.reshape(x3 * inv[:, :, None], [BLOCK_R, G * 32])
|
||||
tl.store(
|
||||
q_ptr + r[:, None].to(tl.int64) * k + c[None, :],
|
||||
q.to(tl.float8e4nv),
|
||||
mask=mask,
|
||||
)
|
||||
smask = rmask[:, None] & (g < n_groups)[None, :]
|
||||
tl.store(
|
||||
s_ptr + _mx_scale_offsets(r[:, None], g[None, :], n_col_blocks),
|
||||
sbyte.to(tl.uint8),
|
||||
mask=smask,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _silu_mul_mxfp8_kernel(
|
||||
x_ptr,
|
||||
q_ptr,
|
||||
s_ptr,
|
||||
rows,
|
||||
hidden,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
stride_row,
|
||||
BLOCK_R: tl.constexpr,
|
||||
G: tl.constexpr,
|
||||
):
|
||||
pid_r = tl.program_id(0)
|
||||
pid_c = tl.program_id(1)
|
||||
r = pid_r * BLOCK_R + tl.arange(0, BLOCK_R)
|
||||
g = pid_c * G + tl.arange(0, G)
|
||||
c = pid_c * (G * 32) + tl.arange(0, G * 32)
|
||||
rmask = r < rows
|
||||
mask = rmask[:, None] & (c < hidden)[None, :]
|
||||
base = x_ptr + r[:, None].to(tl.int64) * stride_row
|
||||
gate = tl.load(base + c[None, :], mask=mask, other=0.0).to(tl.float32)
|
||||
up = tl.load(base + hidden + c[None, :], mask=mask, other=0.0).to(tl.float32)
|
||||
act = (gate * tl.sigmoid(gate)).to(tl.bfloat16).to(tl.float32)
|
||||
prod = (act * up).to(tl.bfloat16).to(tl.float32)
|
||||
p3 = tl.reshape(prod, [BLOCK_R, G, 32])
|
||||
amax = tl.max(tl.abs(p3), axis=2)
|
||||
sbyte, inv = _mx_e8m0_from_amax(amax)
|
||||
q = tl.reshape(p3 * inv[:, :, None], [BLOCK_R, G * 32])
|
||||
tl.store(
|
||||
q_ptr + r[:, None].to(tl.int64) * hidden + c[None, :],
|
||||
q.to(tl.float8e4nv),
|
||||
mask=mask,
|
||||
)
|
||||
smask = rmask[:, None] & (g < n_groups)[None, :]
|
||||
tl.store(
|
||||
s_ptr + _mx_scale_offsets(r[:, None], g[None, :], n_col_blocks),
|
||||
sbyte.to(tl.uint8),
|
||||
mask=smask,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _indexed_scale_shift_mxfp8_kernel(
|
||||
x_ptr,
|
||||
q_ptr,
|
||||
s_ptr,
|
||||
shift_ptr,
|
||||
scale_ptr,
|
||||
indices_ptr,
|
||||
hidden_size,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
stride_x_row,
|
||||
stride_shift_row,
|
||||
stride_scale_row,
|
||||
stride_indices,
|
||||
STORE_BF16: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
columns = tl.arange(0, BLOCK_N)
|
||||
mask = columns < hidden_size
|
||||
index = tl.load(indices_ptr + row * stride_indices)
|
||||
xrow = x_ptr + row.to(tl.int64) * stride_x_row
|
||||
x = tl.load(xrow + columns, mask=mask, other=0.0).to(tl.float32)
|
||||
shift = tl.load(
|
||||
shift_ptr + index * stride_shift_row + columns, mask=mask, other=0.0
|
||||
).to(tl.float32)
|
||||
scale = tl.load(
|
||||
scale_ptr + index * stride_scale_row + columns, mask=mask, other=0.0
|
||||
).to(tl.float32)
|
||||
# the rounding points of _indexed_scale_shift_bf16_kernel
|
||||
one_plus_scale = round_bf16_to_fp32(1.0 + scale)
|
||||
scaled = round_bf16_to_fp32(x * one_plus_scale)
|
||||
out = round_bf16_to_fp32(scaled + shift)
|
||||
if STORE_BF16:
|
||||
tl.store(xrow + columns, out, mask=mask)
|
||||
v3 = tl.reshape(out, [BLOCK_N // 32, 32])
|
||||
amax = tl.max(tl.abs(v3), axis=1)
|
||||
sbyte, inv = _mx_e8m0_from_amax(amax)
|
||||
q = tl.reshape(v3 * inv[:, None], [BLOCK_N])
|
||||
tl.store(
|
||||
q_ptr + row.to(tl.int64) * hidden_size + columns,
|
||||
q.to(tl.float8e4nv),
|
||||
mask=mask,
|
||||
)
|
||||
g = tl.arange(0, BLOCK_N // 32)
|
||||
tl.store(
|
||||
s_ptr + _mx_scale_offsets(row, g, n_col_blocks),
|
||||
sbyte.to(tl.uint8),
|
||||
mask=g < n_groups,
|
||||
)
|
||||
|
||||
|
||||
def can_use_mxfp8_swizzled(x: torch.Tensor) -> bool:
|
||||
"""Row-major bf16 CUDA 2D tensor with K % 32 == 0, outside torch.compile."""
|
||||
return (
|
||||
x.is_cuda
|
||||
and x.ndim == 2
|
||||
and x.dtype == torch.bfloat16
|
||||
and x.stride(-1) == 1
|
||||
and x.shape[-1] % 32 == 0
|
||||
and not torch.compiler.is_compiling()
|
||||
)
|
||||
|
||||
|
||||
def _alloc(
|
||||
rows: int, k: int, device: torch.device
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
q = torch.empty(rows, k, dtype=_E4M3, device=device)
|
||||
s = torch.zeros(_scale_numel(rows, k), dtype=torch.uint8, device=device)
|
||||
return q, s
|
||||
|
||||
|
||||
def mxfp8_quantize_swizzled(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""bf16 ``[rows, k]`` -> ``(fp8 [rows, k], swizzled e8m0 scale bytes)``."""
|
||||
if not can_use_mxfp8_swizzled(x):
|
||||
raise ValueError("expected a row-major bf16 CUDA [rows, k] tensor, k % 32 == 0")
|
||||
rows, k = x.shape
|
||||
q, s = _alloc(rows, k, x.device)
|
||||
if rows == 0:
|
||||
return q, s
|
||||
n_groups = k // 32
|
||||
n_col_blocks = -(-n_groups // 4)
|
||||
block_r, g = 32, 8
|
||||
grid = (triton.cdiv(rows, block_r), triton.cdiv(n_groups, g))
|
||||
with torch.get_device_module().device(x.device):
|
||||
_mxfp8_quant_kernel[grid](
|
||||
x,
|
||||
q,
|
||||
s,
|
||||
rows,
|
||||
k,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
x.stride(0),
|
||||
BLOCK_R=block_r,
|
||||
G=g,
|
||||
num_warps=4,
|
||||
)
|
||||
return q, s
|
||||
|
||||
|
||||
def can_use_silu_mul_mxfp8(hidden: torch.Tensor) -> bool:
|
||||
return can_use_mxfp8_swizzled(hidden) and (hidden.shape[-1] // 2) % 32 == 0
|
||||
|
||||
|
||||
def silu_mul_mxfp8(hidden: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""``hidden [rows, 2n]`` bf16 (gate | up) -> quantized ``silu(gate) * up``."""
|
||||
if not can_use_silu_mul_mxfp8(hidden):
|
||||
raise ValueError(
|
||||
"expected a row-major bf16 CUDA [rows, 2 * n] tensor, n % 32 == 0"
|
||||
)
|
||||
rows, twice = hidden.shape
|
||||
n = twice // 2
|
||||
q, s = _alloc(rows, n, hidden.device)
|
||||
if rows == 0:
|
||||
return q, s
|
||||
n_groups = n // 32
|
||||
n_col_blocks = -(-n_groups // 4)
|
||||
block_r, g = 16, 8
|
||||
grid = (triton.cdiv(rows, block_r), triton.cdiv(n_groups, g))
|
||||
with torch.get_device_module().device(hidden.device):
|
||||
_silu_mul_mxfp8_kernel[grid](
|
||||
hidden,
|
||||
q,
|
||||
s,
|
||||
rows,
|
||||
n,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
hidden.stride(0),
|
||||
BLOCK_R=block_r,
|
||||
G=g,
|
||||
num_warps=4,
|
||||
)
|
||||
return q, s
|
||||
|
||||
|
||||
def indexed_scale_shift_mxfp8_(
|
||||
x: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
indices: torch.Tensor,
|
||||
*,
|
||||
keep_bf16: bool,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor, torch.Tensor]:
|
||||
"""``x * (1 + scale[idx]) + shift[idx]`` -> ``(x | None, fp8, scales)``; with
|
||||
``keep_bf16`` the bf16 result is also written into ``x`` and returned."""
|
||||
if not can_use_mxfp8_swizzled(x):
|
||||
raise ValueError(
|
||||
"expected a row-major bf16 CUDA [rows, hidden] tensor, hidden % 32 == 0"
|
||||
)
|
||||
rows, hidden_size = x.shape
|
||||
q, s = _alloc(rows, hidden_size, x.device)
|
||||
if rows == 0:
|
||||
return (x if keep_bf16 else None), q, s
|
||||
n_groups = hidden_size // 32
|
||||
n_col_blocks = -(-n_groups // 4)
|
||||
block_n = triton.next_power_of_2(hidden_size)
|
||||
with torch.get_device_module().device(x.device):
|
||||
_indexed_scale_shift_mxfp8_kernel[(rows,)](
|
||||
x,
|
||||
q,
|
||||
s,
|
||||
shift,
|
||||
scale,
|
||||
indices,
|
||||
hidden_size,
|
||||
n_groups,
|
||||
n_col_blocks,
|
||||
x.stride(0),
|
||||
shift.stride(0),
|
||||
scale.stride(0),
|
||||
indices.stride(0),
|
||||
STORE_BF16=keep_bf16,
|
||||
BLOCK_N=block_n,
|
||||
num_warps=8,
|
||||
)
|
||||
return (x if keep_bf16 else None), q, s
|
||||
|
||||
|
||||
__all__ = [
|
||||
"can_use_mxfp8_swizzled",
|
||||
"can_use_silu_mul_mxfp8",
|
||||
"indexed_scale_shift_mxfp8_",
|
||||
"mxfp8_quantize_swizzled",
|
||||
"silu_mul_mxfp8",
|
||||
]
|
||||
@@ -33,6 +33,7 @@ def _jit_qknorm_rope_module(
|
||||
round_norm_before_rope: bool,
|
||||
pack_kv: bool = False,
|
||||
cache_has_full_width: bool = False,
|
||||
out_of_place: bool = False,
|
||||
) -> Module:
|
||||
args = make_cpp_args(
|
||||
head_dim,
|
||||
@@ -44,8 +45,12 @@ def _jit_qknorm_rope_module(
|
||||
round_norm_before_rope,
|
||||
cache_has_full_width,
|
||||
)
|
||||
op_name = "qknorm_rope_pack_kv" if pack_kv else "qknorm_rope"
|
||||
kernel_name = "QKNormRopePackKVKernel" if pack_kv else "QKNormRopeKernel"
|
||||
if pack_kv:
|
||||
op_name, kernel_name = "qknorm_rope_pack_kv", "QKNormRopePackKVKernel"
|
||||
elif out_of_place:
|
||||
op_name, kernel_name = "qknorm_rope_out_of_place", "QKNormRopeOutOfPlaceKernel"
|
||||
else:
|
||||
op_name, kernel_name = "qknorm_rope", "QKNormRopeKernel"
|
||||
return load_jit(
|
||||
op_name,
|
||||
*args,
|
||||
@@ -182,6 +187,46 @@ def fused_inplace_qknorm_rope(
|
||||
module.qknorm_rope(q, k, q_weight, k_weight, cos_sin_cache, positions, eps)
|
||||
|
||||
|
||||
@register_custom_op(mutates_args=["q_out", "k_out"])
|
||||
def fused_qknorm_rope_out_of_place(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_out: torch.Tensor,
|
||||
k_out: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
*,
|
||||
is_neox: bool,
|
||||
eps: float = 1e-6,
|
||||
head_dim: int = 0,
|
||||
rope_dim: int = 0,
|
||||
round_norm_before_rope: bool = False,
|
||||
cache_has_full_width: bool = False,
|
||||
) -> None:
|
||||
"""QK-norm + RoPE from ``q``/``k`` (any strides) into ``q_out``/``k_out``;
|
||||
the inputs are left untouched. Same arithmetic as the in-place kernel."""
|
||||
head_dim = head_dim or q.size(-1)
|
||||
if not rope_dim:
|
||||
cache_width = cos_sin_cache.size(-1)
|
||||
rope_dim = cache_width // 2 if cache_has_full_width else cache_width
|
||||
module = _jit_qknorm_rope_module(
|
||||
head_dim,
|
||||
rope_dim,
|
||||
is_neox,
|
||||
q.dtype,
|
||||
cos_sin_cache.dtype,
|
||||
round_norm_before_rope,
|
||||
False,
|
||||
cache_has_full_width,
|
||||
True,
|
||||
)
|
||||
module.qknorm_rope_out_of_place(
|
||||
q, k, q_out, k_out, q_weight, k_weight, cos_sin_cache, positions, eps
|
||||
)
|
||||
|
||||
|
||||
@register_custom_op(mutates_args=["q", "packed_kv"])
|
||||
def fused_qknorm_rope_pack_kv(
|
||||
q: torch.Tensor,
|
||||
|
||||
+27
@@ -452,6 +452,33 @@ MODELS = {
|
||||
],
|
||||
"force_eager": True,
|
||||
},
|
||||
# OpenVDN paper workload: 1344x768, 14.375 s (latent_t 102), t2va, 9 grid points = 8 NFE
|
||||
"vdn-h3": {
|
||||
"path": "OpenVDN/vdn-minimax-h3",
|
||||
"prompt": (
|
||||
"A curious raccoon peers through a vibrant field of yellow "
|
||||
"sunflowers, its eyes wide with interest."
|
||||
),
|
||||
"seed": 1000,
|
||||
"config_overrides": {
|
||||
"task": "t2va",
|
||||
"conditions": [],
|
||||
"target": {
|
||||
"short_edge": 768,
|
||||
"aspect_ratio": "16:9",
|
||||
"duration_seconds": 14.375,
|
||||
},
|
||||
"num_inference_steps": 9,
|
||||
},
|
||||
"extra_args": [
|
||||
"--num-gpus=8",
|
||||
"--quantization=fp8",
|
||||
"--performance-mode=speed",
|
||||
"--enable-torch-compile=false",
|
||||
"--warmup-steps=2",
|
||||
],
|
||||
"force_eager": True,
|
||||
},
|
||||
# Source-tracked extras from current registry / GPU test coverage.
|
||||
"longcat-image": {
|
||||
"path": "meituan-longcat/LongCat-Image",
|
||||
|
||||
@@ -9,7 +9,7 @@ SGLang diffusion features an end-to-end unified pipeline for accelerating diffus
|
||||
## Key Features
|
||||
|
||||
SGLang Diffusion has the following features:
|
||||
- Broad model support: Wan, FastWan, FLUX, Qwen-Image, LongCat-Image, Z-Image, Ideogram 4, Krea-2, Cosmos3, LTX-2/LTX-2.3/LTX-2.5, MiniMax-H3, FastH3, LingBot Video MoE, LingBot World, SANA-Video/SANA-WM, JoyEcho, MOVA, GLM-Image, ERNIE-Image, Hunyuan3D, and more
|
||||
- Broad model support: Wan, FastWan, FLUX, Qwen-Image, LongCat-Image, Z-Image, Ideogram 4, Krea-2, Cosmos3, LTX-2/LTX-2.3/LTX-2.5, MiniMax-H3, FastH3, VDN-H3, LingBot Video MoE, LingBot World, SANA-Video/SANA-WM, JoyEcho, MOVA, GLM-Image, ERNIE-Image, Hunyuan3D, and more
|
||||
- Fast inference speed: empowered by optimized `sgl-kernel` kernels, scheduler/runtime improvements, caching acceleration, and native diffusion hot-path optimizations
|
||||
- Ease of use: OpenAI-compatible api, CLI, and python sdk support
|
||||
- Multi-platform support:
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from sglang.multimodal_gen.configs.models.dits.base import DiTArchConfig, DiTConfig
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3_vdn import (
|
||||
VDNHybridAttentionArchConfig,
|
||||
)
|
||||
|
||||
MINIMAX_H3_PACKED_SEQUENCE_ALIGNMENT = 64
|
||||
MINIMAX_H3_ADALN_MODALITY_NUM = 3
|
||||
@@ -48,6 +51,8 @@ class MiniMaxH3DiTArchConfig(DiTArchConfig):
|
||||
),
|
||||
r"^transformer_blocks\.(\d+)\.attn\.to_out\.0\.(.*)$": r"blocks.\1.attn.out_proj.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn\.to_gate_compress\.(.*)$": r"blocks.\1.attn.to_gate_compress.\2",
|
||||
# VDN-H3 hybrid attention module (see minimax_h3_vdn_attention)
|
||||
r"^transformer_blocks\.(\d+)\.attn\.(linear_attention|softmax_gate|to_out_linear)\.(.*)$": r"blocks.\1.attn.hybrid.\2.\3",
|
||||
r"^transformer_blocks\.(\d+)\.attn\.norm_q\.(.*)$": r"blocks.\1.attn.q_norm.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn\.norm_k\.(.*)$": r"blocks.\1.attn.k_norm.\2",
|
||||
r"^transformer_blocks\.(\d+)\.ff\.net\.0\.proj\.(.*)$": r"blocks.\1.mlp.fc1.\2",
|
||||
@@ -102,6 +107,8 @@ class MiniMaxH3DiTArchConfig(DiTArchConfig):
|
||||
checkpoint_uses_diffusers_layout: bool = False
|
||||
adaln_affine_input_dim: int | None = None
|
||||
has_gate_compress: bool = False
|
||||
# VDN-H3: None for the dense model; set from transformer/config.json
|
||||
hybrid_attention: VDNHybridAttentionArchConfig | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
@@ -110,6 +117,10 @@ class MiniMaxH3DiTArchConfig(DiTArchConfig):
|
||||
if len(self.patch_size) != 3:
|
||||
raise ValueError(f"patch_size must have 3 values, got {self.patch_size}.")
|
||||
self.num_channels_latents = self.latents_dim
|
||||
if isinstance(self.hybrid_attention, dict):
|
||||
self.hybrid_attention = VDNHybridAttentionArchConfig.from_transform_config(
|
||||
self.hybrid_attention
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -133,6 +144,11 @@ class MiniMaxH3DiTConfig(DiTConfig):
|
||||
model_dict["adaln_affine_input_dim"] = source_model_dict["time_embed_dim"]
|
||||
model_dict["time_embed_dim"] = source_model_dict["adaln_rank"]
|
||||
model_dict["adaln_curve_grid"] = source_model_dict["time_table_size"]
|
||||
hybrid = model_dict.get("hybrid_attention")
|
||||
if isinstance(hybrid, dict):
|
||||
model_dict["hybrid_attention"] = (
|
||||
VDNHybridAttentionArchConfig.from_transform_config(hybrid)
|
||||
)
|
||||
super().update_model_arch(model_dict)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 hybrid attention architecture config (window softmax + linear branch)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
|
||||
VDN_H3_DELTA_RULES = ("vdn_solve", "sana_scaled", "vdn_scaled")
|
||||
VDN_H3_BRIDGE_MODES = ("alpha", "none")
|
||||
VDN_H3_ANCHOR_FRAME_MODES = ("none", "columns", "rows", "both")
|
||||
VDN_H3_SHORT_CONV_TARGETS = ("q", "k", "v")
|
||||
|
||||
|
||||
class VDNHybridAttentionArchConfig(msgspec.Struct):
|
||||
"""VDN-H3 hybrid attention (window softmax + frame-wise linear branch); the
|
||||
resolved ``hybrid_attention`` transform config the overlay copies into
|
||||
``transformer/config.json``. A dense checkpoint has none."""
|
||||
|
||||
# frame t is in chunk t // chunk and attends chunks [c - radius, c + radius];
|
||||
# chunk 0 means a centered frame window
|
||||
chunk: int = 5
|
||||
radius: int = 1
|
||||
# "both": frames 0 and F-1 dense as rows and columns, so the branch skips them
|
||||
anchor_frames: str = "both"
|
||||
enable_softmax_gate: bool = True
|
||||
delta_rule: str = "vdn_solve"
|
||||
linear_head_dim: int = 128
|
||||
bridge: str = "alpha"
|
||||
a_fp32: bool = True
|
||||
enable_text_state: bool = True
|
||||
short_conv: tuple[str, ...] = ("k", "v")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.delta_rule not in VDN_H3_DELTA_RULES:
|
||||
raise ValueError(
|
||||
f"hybrid_attention.delta_rule={self.delta_rule!r}; expected one of "
|
||||
f"{VDN_H3_DELTA_RULES}"
|
||||
)
|
||||
if self.bridge not in VDN_H3_BRIDGE_MODES:
|
||||
raise ValueError(
|
||||
f"hybrid_attention.bridge={self.bridge!r}; expected one of "
|
||||
f"{VDN_H3_BRIDGE_MODES}"
|
||||
)
|
||||
if self.anchor_frames not in VDN_H3_ANCHOR_FRAME_MODES:
|
||||
raise ValueError(
|
||||
f"hybrid_attention.anchor_frames={self.anchor_frames!r}; expected "
|
||||
f"one of {VDN_H3_ANCHOR_FRAME_MODES}"
|
||||
)
|
||||
if any(t not in VDN_H3_SHORT_CONV_TARGETS for t in self.short_conv) or len(
|
||||
set(self.short_conv)
|
||||
) != len(self.short_conv):
|
||||
raise ValueError(
|
||||
f"hybrid_attention.short_conv={self.short_conv!r}; expected a "
|
||||
f"distinct subset of {VDN_H3_SHORT_CONV_TARGETS}"
|
||||
)
|
||||
if self.chunk < 0 or self.radius < 0:
|
||||
raise ValueError("hybrid_attention.chunk and radius must be >= 0")
|
||||
if self.linear_head_dim <= 0:
|
||||
raise ValueError("hybrid_attention.linear_head_dim must be positive")
|
||||
|
||||
@classmethod
|
||||
def from_transform_config(
|
||||
cls, config: dict[str, Any]
|
||||
) -> "VDNHybridAttentionArchConfig":
|
||||
"""Build from VDN's nested v2 transform config."""
|
||||
soft = dict(config.get("softmax_attention", {}))
|
||||
lin = dict(config.get("linear_attention", {}))
|
||||
short_conv = lin.get("short_conv", {"targets": []})
|
||||
targets = (
|
||||
short_conv.get("targets", [])
|
||||
if isinstance(short_conv, dict)
|
||||
else list(short_conv or [])
|
||||
)
|
||||
return cls(
|
||||
chunk=int(soft.get("chunk", 0)),
|
||||
radius=int(soft["radius"]),
|
||||
anchor_frames=str(config.get("anchor_frames", "none")),
|
||||
enable_softmax_gate=bool(config.get("enable_softmax_gate", True)),
|
||||
delta_rule=str(lin.get("delta_rule", "vdn_solve")),
|
||||
linear_head_dim=int(lin["linear_head_dim"]),
|
||||
bridge=str(lin.get("bridge", "alpha")),
|
||||
a_fp32=bool(lin.get("a_fp32", True)),
|
||||
enable_text_state=bool(lin.get("enable_text_state", False)),
|
||||
short_conv=tuple(targets),
|
||||
)
|
||||
|
||||
def window_bounds(self, num_frames: int) -> list[tuple[int, int]]:
|
||||
"""Per-frame inclusive softmax-window bounds [lo, hi], unclamped."""
|
||||
if self.chunk <= 0:
|
||||
return [(t - self.radius, t + self.radius) for t in range(num_frames)]
|
||||
return [
|
||||
(
|
||||
((t // self.chunk) - self.radius) * self.chunk,
|
||||
((t // self.chunk) + self.radius + 1) * self.chunk - 1,
|
||||
)
|
||||
for t in range(num_frames)
|
||||
]
|
||||
|
||||
def full_cover(self, num_frames: int) -> bool:
|
||||
"""True when every frame's window already spans the whole clip, i.e.
|
||||
the softmax branch IS dense attention and the linear branch is off."""
|
||||
return all(
|
||||
lo <= 0 and hi >= num_frames - 1
|
||||
for lo, hi in self.window_bounds(num_frames)
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["VDNHybridAttentionArchConfig"]
|
||||
@@ -0,0 +1,76 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 pipeline config: the MiniMax-H3 deployment envelope for the hybrid
|
||||
attention checkpoint."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import (
|
||||
MiniMaxH3PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import (
|
||||
AttentionBackendEnum,
|
||||
current_platform,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VDNH3PipelineConfig(MiniMaxH3PipelineConfig):
|
||||
"""VDN-H3 (hybrid window-softmax + Video Delta linear attention, 8-NFE DMD2
|
||||
distill; t2va and fl2va on one fl2va partition): the deployment envelope;
|
||||
the arch config comes from the materialized ``transformer/config.json``."""
|
||||
|
||||
def validate_quality_deployment(self, server_args) -> None:
|
||||
raise ValueError(
|
||||
'quality="high" is audited only for the base MiniMax-H3 50-step '
|
||||
"4xH200 deployment; the VDN-H3 8-step hybrid checkpoint has no "
|
||||
'audited high-quality deployment. Use quality="lossless".'
|
||||
)
|
||||
|
||||
def validate_server_args(self, server_args) -> None:
|
||||
if server_args.model_variant is not None:
|
||||
raise ValueError(
|
||||
"VDN-H3 ships one weight partition (fl2va, serving t2va and "
|
||||
"fl2va); --model-variant does not apply. Ref2VA was not trained; "
|
||||
"use MiniMaxAI/MiniMax-H3 --model-variant ref2va."
|
||||
)
|
||||
quantization = (server_args.quantization or "").lower()
|
||||
if quantization in ("none", "bf16"):
|
||||
server_args.quantization = None
|
||||
elif (
|
||||
current_platform.is_blackwell() or current_platform.is_sm120()
|
||||
) and quantization in ("", "fp8"):
|
||||
# the block-scaled GEMM exists on SM100+ only; before that fp8 stays per-channel
|
||||
server_args.quantization = "mxfp8"
|
||||
# an unset backend would resolve to the platform default (dense FA)
|
||||
if server_args.attention_backend is None and not (
|
||||
server_args.component_attention_backends or {}
|
||||
).get("transformer"):
|
||||
server_args.attention_backend = "hybrid_window_attn_h3"
|
||||
selected_backend = self.resolve_transformer_attention_backend(server_args)
|
||||
if selected_backend is not AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3:
|
||||
# the base-H3+LoRA equivalence smoke runs plain attention on purpose
|
||||
config = server_args.attention_backend_config or {}
|
||||
if not bool(config.get("vdn_h3_dense_smoke", False)):
|
||||
raise ValueError(
|
||||
"VDN-H3 requires --attention-backend hybrid_window_attn_h3 for "
|
||||
f"the transformer (got {selected_backend}); a dense backend "
|
||||
"would skip the linear branch and the softmax gates and "
|
||||
"produce the wrong model, not a slower one. Pass "
|
||||
"--attention-backend-config '{\"vdn_h3_dense_smoke\": true}' "
|
||||
"only for the base-H3+LoRA equivalence smoke."
|
||||
)
|
||||
if int(server_args.ring_degree or 1) > 1:
|
||||
raise ValueError(
|
||||
"VDN-H3 does not support --ring-degree > 1; use Ulysses sequence "
|
||||
"parallelism."
|
||||
)
|
||||
if server_args.enable_torch_compile or server_args.enable_breakable_cuda_graph:
|
||||
# BCG keeps one pool per captured segment and exhausts 183 GB at 104k rows
|
||||
raise ValueError(
|
||||
"VDN-H3 hybrid attention is not validated under torch.compile or "
|
||||
"the breakable CUDA graph yet; disable them."
|
||||
)
|
||||
super().validate_server_args(server_args)
|
||||
|
||||
|
||||
__all__ = ["VDNH3PipelineConfig"]
|
||||
@@ -0,0 +1,36 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 sampling params: the 8-NFE grid on the MiniMax-H3 request surface."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sglang.multimodal_gen.configs.sample.minimax_h3 import MiniMaxH3SamplingParams
|
||||
|
||||
|
||||
@dataclass
|
||||
class VDNH3SamplingParams(MiniMaxH3SamplingParams):
|
||||
"""VDN-H3: nine sigma grid points, i.e. the eight distilled DiT forwards
|
||||
(VDN counts NFEs; SGLang counts sigma grid points). The turbo adapter is
|
||||
only valid at 8 NFE with video shift 12 / audio shift 3 (the defaults)."""
|
||||
|
||||
num_inference_steps: int = 9
|
||||
|
||||
def _validate(self) -> None:
|
||||
super()._validate()
|
||||
if self.num_inference_steps != 9:
|
||||
raise ValueError(
|
||||
"VDN-H3 is distilled for exactly nine sigma grid points (eight DiT "
|
||||
f"forwards); got num_inference_steps={self.num_inference_steps}. "
|
||||
"Use MiniMaxAI/MiniMax-H3 for other schedules."
|
||||
)
|
||||
if self.task is not None and self.task.strip().lower() not in (
|
||||
"t2va",
|
||||
"fl2va",
|
||||
):
|
||||
raise ValueError(
|
||||
"VDN-H3 serves t2va and fl2va; ref2va was not trained (got "
|
||||
f"task={self.task!r}). Use MiniMaxAI/MiniMax-H3 --model-variant "
|
||||
"ref2va for that task."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["VDNH3SamplingParams"]
|
||||
@@ -84,6 +84,9 @@ from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import (
|
||||
LTX23PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2_5 import LTX25PipelineConfig
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3_vdn import (
|
||||
VDNH3PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.mova import (
|
||||
MOVA360PConfig,
|
||||
MOVA720PConfig,
|
||||
@@ -170,6 +173,7 @@ from sglang.multimodal_gen.configs.sample.minimax_h3 import (
|
||||
FastH3SamplingParams,
|
||||
MiniMaxH3SamplingParams,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.sample.minimax_h3_vdn import VDNH3SamplingParams
|
||||
from sglang.multimodal_gen.configs.sample.mova import (
|
||||
MOVA_360P_SamplingParams,
|
||||
MOVA_720P_SamplingParams,
|
||||
@@ -350,6 +354,7 @@ KNOWN_NON_DIFFUSERS_DIFFUSION_MODEL_PATTERNS: Dict[str, str] = {
|
||||
"minimaxai/minimax-h3": "MiniMaxH3Pipeline",
|
||||
"minimax/minimax-h3": "MiniMaxH3Pipeline",
|
||||
"fastvideo/fastvideo-fasth3-4-step-preview-v1-vsa-datafree": "FastH3Pipeline",
|
||||
"openvdn/vdn-minimax-h3": "VDNH3Pipeline",
|
||||
"lerobot/pi05": "Pi05Pipeline",
|
||||
"pi05": "Pi05Pipeline",
|
||||
"pi0.5": "Pi05Pipeline",
|
||||
@@ -1016,6 +1021,7 @@ def _register_configs():
|
||||
model_detectors=[
|
||||
lambda model_id: (
|
||||
"minimaxh3" in model_id.lower().replace("-", "").replace("_", "")
|
||||
and "vdn" not in model_id.lower()
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -1038,6 +1044,19 @@ def _register_configs():
|
||||
)
|
||||
],
|
||||
)
|
||||
register_configs(
|
||||
sampling_param_cls=VDNH3SamplingParams,
|
||||
pipeline_config_cls=VDNH3PipelineConfig,
|
||||
hf_model_paths=[
|
||||
"OpenVDN/vdn-minimax-h3",
|
||||
],
|
||||
model_detectors=[
|
||||
lambda model_id: (
|
||||
"vdn" in model_id.lower()
|
||||
and "minimaxh3" in model_id.lower().replace("-", "").replace("_", "")
|
||||
)
|
||||
],
|
||||
)
|
||||
# FLUX
|
||||
register_configs(
|
||||
sampling_param_cls=FluxSamplingParams,
|
||||
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 window-softmax backend on the MiniMax-H3 packed layout.
|
||||
|
||||
An exact softmax over a chunk-aligned frame window: frame t belongs to chunk
|
||||
t // chunk and attends to chunks [c - radius, c + radius]; frames 0 and F-1
|
||||
are dense anchors; text and audio rows are dense both ways; padding rows sit
|
||||
outside every mask; a per-(token, head) sigmoid gate scales the output. The
|
||||
linear branch (``minimax_h3_vdn.py``) covers the window's complement. The
|
||||
metadata is request-static and installed once per request through
|
||||
``set_forward_context``. The window runs as a union of dense varlen
|
||||
FlashAttention calls: the dense-query rows against all keys, then per-chunk
|
||||
gathered [globals | window | anchors] K/V; same math as a masked kernel up to
|
||||
bf16 reduction order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.flash_attention import flash_attn_varlen_func
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3_vdn import (
|
||||
VDNHybridAttentionArchConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends import (
|
||||
flash_attn as _flash_attn_backend,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
|
||||
AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
AttentionMetadataBuilder,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn import VDNH3Layout
|
||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||
|
||||
_DIT_BLOCK_PREFIX = re.compile(r"^blocks\.(\d+)\.")
|
||||
|
||||
|
||||
class HybridWindowAttentionH3Backend(AttentionBackend):
|
||||
accept_output_buffer: bool = False
|
||||
|
||||
@staticmethod
|
||||
def get_supported_head_sizes() -> list[int]:
|
||||
return [64, 128]
|
||||
|
||||
@staticmethod
|
||||
def get_enum() -> AttentionBackendEnum:
|
||||
return AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type[HybridWindowAttentionH3Impl]:
|
||||
return HybridWindowAttentionH3Impl
|
||||
|
||||
@staticmethod
|
||||
def get_metadata_cls() -> type[HybridWindowAttentionH3Metadata]:
|
||||
return HybridWindowAttentionH3Metadata
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type[HybridWindowAttentionH3MetadataBuilder]:
|
||||
return HybridWindowAttentionH3MetadataBuilder
|
||||
|
||||
|
||||
def window_mask_frames(
|
||||
hybrid: VDNHybridAttentionArchConfig, num_frames: int
|
||||
) -> tuple[list[tuple[int, int]], set[int], set[int]]:
|
||||
"""(clamped per-frame window bounds, dense-ROW frames, dense-COLUMN frames)."""
|
||||
bounds = [
|
||||
(max(lo, 0), min(hi, num_frames - 1))
|
||||
for lo, hi in hybrid.window_bounds(num_frames)
|
||||
]
|
||||
anchors = {0, num_frames - 1} if hybrid.anchor_frames != "none" else set()
|
||||
dense_rows = anchors if hybrid.anchor_frames in ("rows", "both") else set()
|
||||
dense_cols = anchors if hybrid.anchor_frames in ("columns", "both") else set()
|
||||
return bounds, dense_rows, dense_cols
|
||||
|
||||
|
||||
def window_mask_reference(
|
||||
hybrid: VDNHybridAttentionArchConfig, layout: VDNH3Layout, device: torch.device
|
||||
) -> torch.Tensor:
|
||||
"""Dense boolean [used, used] mask of the softmax branch, for tests."""
|
||||
used = layout.used
|
||||
keep = torch.ones(used, used, dtype=torch.bool, device=device)
|
||||
vs, ve = layout.video_start, layout.video_end
|
||||
bounds, dense_rows, dense_cols = window_mask_frames(hybrid, layout.num_frames)
|
||||
tpf = layout.tokens_per_frame
|
||||
rows = torch.arange(vs, ve, device=device)
|
||||
frame_of = (rows - vs) // tpf
|
||||
qf = frame_of[:, None]
|
||||
kf = frame_of[None, :]
|
||||
lo = torch.tensor([b[0] for b in bounds], device=device)[qf]
|
||||
hi = torch.tensor([b[1] for b in bounds], device=device)[qf]
|
||||
inside = (kf >= lo) & (kf <= hi)
|
||||
for f in dense_rows:
|
||||
inside |= qf == f
|
||||
for f in dense_cols:
|
||||
inside |= kf == f
|
||||
keep[vs:ve, vs:ve] = inside
|
||||
return keep
|
||||
|
||||
|
||||
def _merge_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
||||
out: list[tuple[int, int]] = []
|
||||
for a, b in sorted(ranges):
|
||||
if out and out[-1][1] >= a:
|
||||
out[-1] = (out[-1][0], max(out[-1][1], b))
|
||||
else:
|
||||
out.append((a, b))
|
||||
return out
|
||||
|
||||
|
||||
def _cat_ranges(ranges: list[tuple[int, int]], *, device: torch.device) -> torch.Tensor:
|
||||
if not ranges:
|
||||
return torch.empty(0, dtype=torch.long, device=device)
|
||||
return torch.cat(
|
||||
[torch.arange(a, b, device=device, dtype=torch.long) for a, b in ranges]
|
||||
)
|
||||
|
||||
|
||||
def _chunk_groups(
|
||||
raw_bounds: list[tuple[int, int]], dense_rows: set[int]
|
||||
) -> list[list[int]]:
|
||||
# consecutive window frames with identical bounds share one varlen segment
|
||||
groups: list[list[int]] = []
|
||||
for f in range(len(raw_bounds)):
|
||||
if f in dense_rows:
|
||||
continue
|
||||
if (
|
||||
groups
|
||||
and raw_bounds[groups[-1][-1]] == raw_bounds[f]
|
||||
and groups[-1][-1] == f - 1
|
||||
):
|
||||
groups[-1].append(f)
|
||||
else:
|
||||
groups.append([f])
|
||||
return groups
|
||||
|
||||
|
||||
class _ChunkGroup(msgspec.Struct, frozen=True):
|
||||
frames: list[int]
|
||||
query_rows: torch.Tensor
|
||||
kv_rows: torch.Tensor
|
||||
|
||||
|
||||
class _WindowPass(msgspec.Struct, frozen=True):
|
||||
query_rows: torch.Tensor
|
||||
query_slice: tuple[int, int] | None # set when the query rows are contiguous
|
||||
kv_rows: torch.Tensor
|
||||
cu_q: torch.Tensor
|
||||
cu_k: torch.Tensor
|
||||
max_q: int
|
||||
max_k: int
|
||||
|
||||
|
||||
def _window_pass(
|
||||
layout: VDNH3Layout, groups: list[_ChunkGroup], device: torch.device
|
||||
) -> _WindowPass:
|
||||
query_lens = [int(group.query_rows.numel()) for group in groups]
|
||||
kv_lens = [int(group.kv_rows.numel()) for group in groups]
|
||||
frames = [frame for group in groups for frame in group.frames]
|
||||
contiguous = frames == list(range(frames[0], frames[0] + len(frames)))
|
||||
zero = torch.zeros(1, dtype=torch.long)
|
||||
return _WindowPass(
|
||||
query_rows=torch.cat([group.query_rows for group in groups]),
|
||||
query_slice=(
|
||||
(layout.frame_rows(frames[0])[0], layout.frame_rows(frames[-1])[1])
|
||||
if contiguous
|
||||
else None
|
||||
),
|
||||
kv_rows=torch.cat([group.kv_rows for group in groups]),
|
||||
cu_q=torch.cat([zero, torch.tensor(query_lens).cumsum(0)]).to(
|
||||
device, torch.int32
|
||||
),
|
||||
cu_k=torch.cat([zero, torch.tensor(kv_lens).cumsum(0)]).to(device, torch.int32),
|
||||
max_q=max(query_lens),
|
||||
max_k=max(kv_lens),
|
||||
)
|
||||
|
||||
|
||||
def _window_passes(
|
||||
layout: VDNH3Layout,
|
||||
groups: list[_ChunkGroup],
|
||||
max_gather_rows: int,
|
||||
device: torch.device,
|
||||
) -> list[_WindowPass]:
|
||||
# one varlen call per pass; consecutive chunk groups fill up to max_gather_rows
|
||||
passes: list[_WindowPass] = []
|
||||
current: list[_ChunkGroup] = []
|
||||
current_rows = 0
|
||||
for group in groups:
|
||||
rows = int(group.kv_rows.numel())
|
||||
if current and current_rows + rows > max_gather_rows:
|
||||
passes.append(_window_pass(layout, current, device))
|
||||
current, current_rows = [], 0
|
||||
current.append(group)
|
||||
current_rows += rows
|
||||
if current:
|
||||
passes.append(_window_pass(layout, current, device))
|
||||
return passes
|
||||
|
||||
|
||||
class _DecomposedPlan:
|
||||
"""Query-row groups with identical kept key sets, as dense varlen calls:
|
||||
the dense-q rows against all ``used`` keys, then each chunk of frames
|
||||
against its gathered [globals | window | anchors] keys."""
|
||||
|
||||
__slots__ = ("dense_q", "dense_cu_q", "dense_cu_k", "passes")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
layout: VDNH3Layout,
|
||||
hybrid: VDNHybridAttentionArchConfig,
|
||||
device: torch.device,
|
||||
max_gather_rows: int = 200_000,
|
||||
) -> None:
|
||||
used, num_frames = layout.used, layout.num_frames
|
||||
bounds, dense_rows, dense_cols = window_mask_frames(hybrid, num_frames)
|
||||
rows = functools.partial(_cat_ranges, device=device)
|
||||
dense_ranges = _merge_ranges(
|
||||
layout.global_ranges + [layout.frame_rows(f) for f in sorted(dense_rows)]
|
||||
)
|
||||
self.dense_q = rows(dense_ranges)
|
||||
# built once: a tensor from a Python list costs a pageable H2D copy + sync
|
||||
self.dense_cu_q = torch.tensor(
|
||||
[0, int(self.dense_q.numel())], dtype=torch.int32, device=device
|
||||
)
|
||||
self.dense_cu_k = torch.tensor([0, used], dtype=torch.int32, device=device)
|
||||
groups = []
|
||||
for frames in _chunk_groups(hybrid.window_bounds(num_frames), dense_rows):
|
||||
lo, hi = bounds[frames[0]]
|
||||
kv_frames = sorted(set(range(lo, hi + 1)) | dense_cols)
|
||||
groups.append(
|
||||
_ChunkGroup(
|
||||
frames=frames,
|
||||
query_rows=rows(
|
||||
_merge_ranges([layout.frame_rows(f) for f in frames])
|
||||
),
|
||||
kv_rows=rows(
|
||||
_merge_ranges(
|
||||
layout.global_ranges
|
||||
+ [layout.frame_rows(f) for f in kv_frames]
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
self.passes = _window_passes(layout, groups, max_gather_rows, device)
|
||||
window_rows = sum(int(p.query_rows.numel()) for p in self.passes)
|
||||
covered = int(self.dense_q.numel()) + window_rows
|
||||
if covered != used:
|
||||
raise ValueError(
|
||||
f"window decomposition covers {covered} of {used} packed rows"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HybridWindowAttentionH3Metadata(AttentionMetadata):
|
||||
layout: VDNH3Layout
|
||||
# radius >= F: the window IS dense attention and the linear branch is off
|
||||
full_cover: bool
|
||||
decomposed: _DecomposedPlan | None = None
|
||||
# (cos_sin [seq_len, rope_dim] bf16, positions [seq_len]) under Ulysses, else None
|
||||
rope_cache_full: tuple[torch.Tensor, torch.Tensor] | None = None
|
||||
|
||||
|
||||
class HybridWindowAttentionH3MetadataBuilder(AttentionMetadataBuilder):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def prepare(self) -> None:
|
||||
pass
|
||||
|
||||
def build( # type: ignore[override]
|
||||
self,
|
||||
*,
|
||||
layout: VDNH3Layout,
|
||||
hybrid: VDNHybridAttentionArchConfig,
|
||||
device: torch.device,
|
||||
rope_cache_full: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
current_timestep: int = 0,
|
||||
max_gather_rows: int = 200_000,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> HybridWindowAttentionH3Metadata:
|
||||
full_cover = hybrid.full_cover(layout.num_frames)
|
||||
decomposed = None
|
||||
if not full_cover:
|
||||
decomposed = _DecomposedPlan(
|
||||
layout, hybrid, device, max_gather_rows=max_gather_rows
|
||||
)
|
||||
return HybridWindowAttentionH3Metadata(
|
||||
current_timestep=current_timestep,
|
||||
layout=layout,
|
||||
full_cover=full_cover,
|
||||
decomposed=decomposed,
|
||||
rope_cache_full=rope_cache_full,
|
||||
)
|
||||
|
||||
|
||||
def _fa_varlen(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
*,
|
||||
cu_q: torch.Tensor,
|
||||
cu_k: torch.Tensor,
|
||||
max_q: int,
|
||||
max_k: int,
|
||||
scale: float,
|
||||
out: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
attn_out = flash_attn_varlen_func(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens_q=cu_q,
|
||||
cu_seqlens_k=cu_k,
|
||||
max_seqlen_q=max_q,
|
||||
max_seqlen_k=max_k,
|
||||
softmax_scale=scale,
|
||||
causal=False,
|
||||
ver=_flash_attn_backend.fa_ver,
|
||||
out=out,
|
||||
)
|
||||
attn_out = attn_out[0] if isinstance(attn_out, tuple) else attn_out
|
||||
if out is not None and attn_out.data_ptr() != out.data_ptr():
|
||||
out.copy_(attn_out)
|
||||
return out
|
||||
return attn_out
|
||||
|
||||
|
||||
class HybridWindowAttentionH3Impl(AttentionImpl):
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
causal: bool,
|
||||
softmax_scale: float,
|
||||
num_kv_heads: int | None = None,
|
||||
prefix: str = "",
|
||||
**extra_impl_args,
|
||||
) -> None:
|
||||
self.num_heads = num_heads
|
||||
self.head_size = head_size
|
||||
self.softmax_scale = softmax_scale
|
||||
self.prefix = prefix
|
||||
match = _DIT_BLOCK_PREFIX.match(prefix)
|
||||
self.layer_idx = int(match.group(1)) if match else None
|
||||
# non-DiT callers (the token refiner) resolve this backend too: dense FA
|
||||
self._dense_fallback = _flash_attn_backend.FlashAttentionImpl(
|
||||
num_heads=num_heads,
|
||||
head_size=head_size,
|
||||
causal=causal,
|
||||
softmax_scale=softmax_scale,
|
||||
num_kv_heads=num_kv_heads,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""Dense FlashAttention for the non-DiT layers this backend reaches."""
|
||||
return self._dense_fallback.forward(query, key, value, attn_metadata)
|
||||
|
||||
def dense_varlen(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
*,
|
||||
cu_seqlens: torch.Tensor,
|
||||
max_seqlen: int,
|
||||
cu_seqlens_host: tuple[int, ...] | None = None,
|
||||
) -> torch.Tensor:
|
||||
return self._dense_fallback.forward_varlen(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
cu_seqlens=cu_seqlens,
|
||||
max_seqlen=max_seqlen,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
)
|
||||
|
||||
def forward_varlen(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
*,
|
||||
cu_seqlens: torch.Tensor,
|
||||
max_seqlen: int,
|
||||
cu_seqlens_host: tuple[int, ...] | None = None,
|
||||
attn_metadata: HybridWindowAttentionH3Metadata | None = None,
|
||||
softmax_gate: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""query/key/value: [T, H, D] packed rows (post-norm, post-RoPE) ->
|
||||
[T, H, D]; ``softmax_gate`` [T, H] scales the output per (row, head).
|
||||
Rows at and past ``used`` (padding) are zero."""
|
||||
if self.layer_idx is not None and attn_metadata is None:
|
||||
raise RuntimeError(
|
||||
"hybrid_window_attn_h3 needs per-request attention metadata "
|
||||
"from the MiniMax-H3 denoising stage; none was set in the "
|
||||
"forward context."
|
||||
)
|
||||
if self.layer_idx is None:
|
||||
return self.dense_varlen(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
cu_seqlens=cu_seqlens,
|
||||
max_seqlen=max_seqlen,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
)
|
||||
|
||||
meta = attn_metadata
|
||||
layout = meta.layout
|
||||
bounds = (
|
||||
cu_seqlens_host
|
||||
if cu_seqlens_host is not None
|
||||
else tuple(int(item) for item in cu_seqlens.tolist())
|
||||
)
|
||||
used = int(bounds[1])
|
||||
if used != layout.used or query.shape[0] != layout.seq_len:
|
||||
raise ValueError(
|
||||
f"hybrid_window_attn_h3 metadata was built for used={layout.used} "
|
||||
f"of seq_len={layout.seq_len} rows, got used={used} of "
|
||||
f"{query.shape[0]}. The request metadata and the packed layout "
|
||||
"have diverged."
|
||||
)
|
||||
|
||||
if meta.full_cover:
|
||||
out = self.dense_varlen(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
cu_seqlens=cu_seqlens,
|
||||
max_seqlen=max_seqlen,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
)
|
||||
else:
|
||||
out = self._decomposed(query, key, value, meta.decomposed, used)
|
||||
|
||||
if softmax_gate is not None:
|
||||
out.mul_(softmax_gate.to(out.dtype).unsqueeze(-1))
|
||||
if used < out.shape[0]:
|
||||
out[used:].zero_()
|
||||
return out
|
||||
|
||||
def _decomposed(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
plan: _DecomposedPlan,
|
||||
used: int,
|
||||
) -> torch.Tensor:
|
||||
out = torch.empty_like(query)
|
||||
key_used = key[:used]
|
||||
value_used = value[:used]
|
||||
if not key_used.is_contiguous():
|
||||
key_used = key_used.contiguous()
|
||||
if not value_used.is_contiguous():
|
||||
value_used = value_used.contiguous()
|
||||
if plan.dense_q.numel():
|
||||
out[plan.dense_q] = _fa_varlen(
|
||||
torch.index_select(query, 0, plan.dense_q),
|
||||
key_used,
|
||||
value_used,
|
||||
cu_q=plan.dense_cu_q,
|
||||
cu_k=plan.dense_cu_k,
|
||||
max_q=int(plan.dense_q.numel()),
|
||||
max_k=used,
|
||||
scale=self.softmax_scale,
|
||||
)
|
||||
for window in plan.passes:
|
||||
# index_select on contiguous copies takes the vectorized gather kernel
|
||||
keys = torch.index_select(key_used, 0, window.kv_rows)
|
||||
values = torch.index_select(value_used, 0, window.kv_rows)
|
||||
if window.query_slice is not None:
|
||||
start, stop = window.query_slice
|
||||
_fa_varlen(
|
||||
query[start:stop],
|
||||
keys,
|
||||
values,
|
||||
cu_q=window.cu_q,
|
||||
cu_k=window.cu_k,
|
||||
max_q=window.max_q,
|
||||
max_k=window.max_k,
|
||||
scale=self.softmax_scale,
|
||||
out=out[start:stop],
|
||||
)
|
||||
else:
|
||||
out[window.query_rows] = _fa_varlen(
|
||||
torch.index_select(query, 0, window.query_rows),
|
||||
keys,
|
||||
values,
|
||||
cu_q=window.cu_q,
|
||||
cu_k=window.cu_k,
|
||||
max_q=window.max_q,
|
||||
max_k=window.max_k,
|
||||
scale=self.softmax_scale,
|
||||
)
|
||||
del keys, values
|
||||
return out
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HybridWindowAttentionH3Backend",
|
||||
"HybridWindowAttentionH3Impl",
|
||||
"HybridWindowAttentionH3Metadata",
|
||||
"HybridWindowAttentionH3MetadataBuilder",
|
||||
"window_mask_frames",
|
||||
"window_mask_reference",
|
||||
]
|
||||
@@ -91,6 +91,11 @@ def adjust_scalar_to_fused_array(
|
||||
class LinearMethodBase(QuantizeMethodBase):
|
||||
"""Base class for different (maybe quantized) linear methods."""
|
||||
|
||||
def accepts_mxfp8_input(self, layer: torch.nn.Module) -> bool:
|
||||
"""Whether ``apply`` takes a prequantized ``(e4m3 input, swizzled E8M0
|
||||
block scales)`` tuple for this layer."""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def create_weights(
|
||||
self,
|
||||
|
||||
@@ -76,6 +76,12 @@ class MXFP8Config(SRTFp8Config, QuantizationConfig):
|
||||
return UnquantizedLinearMethod()
|
||||
if current_platform.is_npu():
|
||||
return NPUMXFP8LinearMethod(self)
|
||||
if not self.is_checkpoint_fp8_serialized:
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.mxfp8_online import (
|
||||
MXFP8OnlineLinearMethod,
|
||||
)
|
||||
|
||||
return MXFP8OnlineLinearMethod(self)
|
||||
return SRTFp8LinearMethod(self)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Online MXFP8 for diffusion linears: ``--quantization mxfp8`` on a bf16 checkpoint.
|
||||
|
||||
Weights quantize at load to e4m3 with one E8M0 scale per 32 elements along K
|
||||
(scales in the cuBLASLt 128x4 swizzled layout, ``mxfp8_quantize_swizzled``);
|
||||
activations take the same block quant per call unless the producer hands over
|
||||
a prequantized ``(fp8, swizzled scales)`` tuple; the GEMM is cuBLASLt's
|
||||
block-scaled ``torch.nn.functional.scaled_mm``. Layers with K not a multiple
|
||||
of 32, N not a multiple of 16, non-bf16 params, or pre-Blackwell GPUs keep
|
||||
the per-channel fp8 path.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch.nn import Module
|
||||
from torch.nn.functional import ScalingType, SwizzleType, scaled_mm
|
||||
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
can_use_mxfp8_swizzled,
|
||||
mxfp8_quantize_swizzled,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.fp8 import Fp8LinearMethod
|
||||
|
||||
_E8M0 = torch.float8_e8m0fnu
|
||||
|
||||
|
||||
def mxfp8_scaled_mm(
|
||||
a: torch.Tensor,
|
||||
a_scale: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
*,
|
||||
bias: Optional[torch.Tensor],
|
||||
output_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
return scaled_mm(
|
||||
a,
|
||||
weight.t(),
|
||||
scale_a=a_scale,
|
||||
scale_recipe_a=ScalingType.BlockWise1x32,
|
||||
swizzle_a=SwizzleType.SWIZZLE_32_4_4,
|
||||
scale_b=weight_scale,
|
||||
scale_recipe_b=ScalingType.BlockWise1x32,
|
||||
swizzle_b=SwizzleType.SWIZZLE_32_4_4,
|
||||
bias=bias,
|
||||
output_dtype=output_dtype,
|
||||
)
|
||||
|
||||
|
||||
class MXFP8OnlineLinearMethod(Fp8LinearMethod):
|
||||
def __init__(self, quant_config) -> None:
|
||||
super().__init__(quant_config)
|
||||
# SRTFp8Config(use_mxfp8) sets a block size; the fallback path loads bf16
|
||||
self.block_quant = False
|
||||
|
||||
def process_weights_after_loading(self, layer: Module) -> None:
|
||||
layer.mxfp8 = (
|
||||
not self.use_marlin
|
||||
and can_use_mxfp8_swizzled(layer.weight)
|
||||
and torch.cuda.get_device_capability(layer.weight.device)[0] >= 10
|
||||
and layer.weight.shape[0] % 16 == 0
|
||||
)
|
||||
if not layer.mxfp8:
|
||||
super().process_weights_after_loading(layer)
|
||||
return
|
||||
qweight, scale = mxfp8_quantize_swizzled(layer.weight.data.contiguous())
|
||||
layer.weight = torch.nn.Parameter(qweight, requires_grad=False)
|
||||
layer.weight_scale = torch.nn.Parameter(scale.view(_E8M0), requires_grad=False)
|
||||
layer.input_scale = None
|
||||
|
||||
def accepts_mxfp8_input(self, layer: Module) -> bool:
|
||||
return bool(layer.mxfp8)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
if not layer.mxfp8:
|
||||
return super().apply(layer, x, bias)
|
||||
if isinstance(x, tuple):
|
||||
a, a_scale = x
|
||||
a_scale = a_scale.view(_E8M0)
|
||||
lead, output_dtype = a.shape[:-1], torch.bfloat16
|
||||
else:
|
||||
lead, output_dtype = x.shape[:-1], x.dtype
|
||||
a, a_scale = mxfp8_quantize_swizzled(
|
||||
x.reshape(-1, x.shape[-1]).contiguous()
|
||||
)
|
||||
a_scale = a_scale.view(_E8M0)
|
||||
out = mxfp8_scaled_mm(
|
||||
a.reshape(-1, a.shape[-1]),
|
||||
a_scale,
|
||||
layer.weight,
|
||||
layer.weight_scale,
|
||||
bias=bias,
|
||||
output_dtype=output_dtype,
|
||||
)
|
||||
return out.view(*lead, out.shape[-1])
|
||||
|
||||
|
||||
__all__ = ["MXFP8OnlineLinearMethod", "mxfp8_scaled_mm"]
|
||||
@@ -22,10 +22,14 @@ from sglang.kernels.ops.activation.activation import (
|
||||
)
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
can_use_fused_inplace_qknorm_rope,
|
||||
can_use_mxfp8_swizzled,
|
||||
can_use_silu_mul_mxfp8,
|
||||
fused_inplace_qknorm_rope,
|
||||
indexed_gate_bf16,
|
||||
indexed_gate_bf16_,
|
||||
indexed_scale_shift_bf16_,
|
||||
indexed_scale_shift_mxfp8_,
|
||||
silu_mul_mxfp8,
|
||||
)
|
||||
from sglang.kernels.ops.layernorm.norm import fused_inplace_qknorm
|
||||
from sglang.multimodal_gen import envs
|
||||
@@ -79,6 +83,9 @@ from sglang.multimodal_gen.runtime.models.dits.minimax_h3_adaln_cache import (
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_adaln_cache import (
|
||||
native_adaln_weight_files,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn_attention import (
|
||||
MiniMaxH3VDNHybridAttention,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.parameter import BlockQuantScaleParameter
|
||||
from sglang.multimodal_gen.runtime.platforms import (
|
||||
AttentionBackendEnum,
|
||||
@@ -362,6 +369,12 @@ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
||||
return torch.cat((-x2, x1), dim=-1)
|
||||
|
||||
|
||||
def _accepts_mxfp8_input(linear: nn.Module) -> bool:
|
||||
return linear.quant_method is not None and linear.quant_method.accepts_mxfp8_input(
|
||||
linear
|
||||
)
|
||||
|
||||
|
||||
def _modulate_scale_shift(
|
||||
x: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
@@ -827,6 +840,10 @@ class MiniMaxH3Attention(nn.Module):
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.to_gate_compress",
|
||||
)
|
||||
# VDN-H3: None for the dense model and for the token refiner
|
||||
self.hybrid = MiniMaxH3VDNHybridAttention.build(
|
||||
arch, quant_config, prefix=prefix, local_heads=self.num_heads
|
||||
)
|
||||
|
||||
def _set_attention_backend(self, backend) -> None:
|
||||
if (
|
||||
@@ -1032,9 +1049,12 @@ class MiniMaxH3Attention(nn.Module):
|
||||
subblock_sparse_query_block_mask: torch.Tensor | None = None,
|
||||
ulysses_active: bool = False,
|
||||
ring_active: bool = False,
|
||||
x_prequant: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""x: [T, hidden] packed thd rows -> [T, hidden].
|
||||
|
||||
``x_prequant``: ``x`` already quantized for ``qkv_proj`` as ``(fp8, scales)``.
|
||||
|
||||
Operation order: fused qkv projection -> per-head q/k RMSNorm -> RoPE
|
||||
on q/k -> variable-length non-causal flash attention -> output projection.
|
||||
|
||||
@@ -1054,11 +1074,29 @@ class MiniMaxH3Attention(nn.Module):
|
||||
)
|
||||
|
||||
total = x.shape[0]
|
||||
qkv, _ = self.qkv_proj(x)
|
||||
qkv, _ = self.qkv_proj(x if x_prequant is None else x_prequant)
|
||||
q, k, v = qkv.split(self.local_inner_dim, dim=-1)
|
||||
q = q.view(total, self.num_heads, self.head_dim)
|
||||
k = k.view(total, self.num_heads, self.head_dim)
|
||||
v = v.view(total, self.num_heads, self.head_dim)
|
||||
if (
|
||||
self.hybrid is not None
|
||||
and self._attention_backend_enum
|
||||
is AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3
|
||||
):
|
||||
return self.hybrid(
|
||||
self,
|
||||
x,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
rope_cache=rope_cache,
|
||||
cu_seqlens=cu_seqlens,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
max_seqlen=max_seqlen,
|
||||
ulysses_active=ulysses_active,
|
||||
ring_active=ring_active,
|
||||
)
|
||||
if rope_cache is None:
|
||||
q, k = _apply_qk_norm(
|
||||
q,
|
||||
@@ -1160,7 +1198,7 @@ class MiniMaxH3MLP(nn.Module):
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if x.device.type == "mps":
|
||||
if not isinstance(x, tuple) and x.device.type == "mps":
|
||||
out = torch.empty_like(x)
|
||||
for start in range(0, x.shape[0], _MPS_MLP_TOKEN_CHUNK_SIZE):
|
||||
stop = min(start + _MPS_MLP_TOKEN_CHUNK_SIZE, x.shape[0])
|
||||
@@ -1173,6 +1211,9 @@ class MiniMaxH3MLP(nn.Module):
|
||||
torch.mps.empty_cache()
|
||||
return out
|
||||
hidden, _ = self.fc1(x)
|
||||
if _accepts_mxfp8_input(self.fc2) and can_use_silu_mul_mxfp8(hidden):
|
||||
out, _ = self.fc2(silu_mul_mxfp8(hidden))
|
||||
return out
|
||||
hidden = _silu_mul(hidden, reuse_input=self.reuse_fc1_activation)
|
||||
out, _ = self.fc2(hidden)
|
||||
return out
|
||||
@@ -1384,11 +1425,20 @@ class MiniMaxH3DiTBlock(nn.Module):
|
||||
# a block-local buffer.
|
||||
residual = x
|
||||
h = self.norm1(x)
|
||||
h = _modulate_scale_shift(
|
||||
h, shift_msa, scale_msa, combined_indices, dtype=_BF16_DTYPE
|
||||
)
|
||||
h_prequant = None
|
||||
if _accepts_mxfp8_input(self.attn.qkv_proj) and can_use_mxfp8_swizzled(h):
|
||||
# the bf16 modulated rows stay in h for the VDN branch projections
|
||||
h, h_fp8, h_scales = indexed_scale_shift_mxfp8_(
|
||||
h, shift_msa, scale_msa, combined_indices, keep_bf16=True
|
||||
)
|
||||
h_prequant = (h_fp8, h_scales)
|
||||
else:
|
||||
h = _modulate_scale_shift(
|
||||
h, shift_msa, scale_msa, combined_indices, dtype=_BF16_DTYPE
|
||||
)
|
||||
h = self.attn(
|
||||
h,
|
||||
x_prequant=h_prequant,
|
||||
rope_cache=rope_cache,
|
||||
cu_seqlens=cu_seqlens,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
@@ -1408,10 +1458,16 @@ class MiniMaxH3DiTBlock(nn.Module):
|
||||
|
||||
residual = x
|
||||
h = self.norm2(x)
|
||||
h = _modulate_scale_shift(
|
||||
h, shift_mlp, scale_mlp, combined_indices, dtype=_BF16_DTYPE
|
||||
)
|
||||
h = self.mlp(h)
|
||||
if _accepts_mxfp8_input(self.mlp.fc1) and can_use_mxfp8_swizzled(h):
|
||||
_, h_fp8, h_scales = indexed_scale_shift_mxfp8_(
|
||||
h, shift_mlp, scale_mlp, combined_indices, keep_bf16=False
|
||||
)
|
||||
h = self.mlp((h_fp8, h_scales))
|
||||
else:
|
||||
h = _modulate_scale_shift(
|
||||
h, shift_mlp, scale_mlp, combined_indices, dtype=_BF16_DTYPE
|
||||
)
|
||||
h = self.mlp(h)
|
||||
# `residual` is block-local here (see above), so this stays in-place
|
||||
# even while Cache-DiT is attached.
|
||||
return _modulate_gate(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,647 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 hybrid attention inside a MiniMax-H3 DiT block: per-head softmax gate,
|
||||
Video Delta linear branch and its output projection, the Ulysses exchange that
|
||||
shards both branches by head, and the request-static attention metadata.
|
||||
``MiniMaxH3Attention`` owns one instance as ``hybrid`` and hands it raw q/k/v."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable, Mapping
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
fused_qknorm_rope_out_of_place,
|
||||
usp_merge_heads,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3 import MiniMaxH3DiTArchConfig
|
||||
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
|
||||
get_ring_ctx,
|
||||
get_ulysses_ctx,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3 import (
|
||||
HybridWindowAttentionH3Metadata,
|
||||
HybridWindowAttentionH3MetadataBuilder,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.linear import RowParallelLinear
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
|
||||
QuantizationConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.usp import _a2a_staging_buffer
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn import (
|
||||
MiniMaxH3VDNLinearBranch,
|
||||
VDNSoftmaxGate,
|
||||
vdn_h3_layout_from_packed,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph import (
|
||||
eager_on_graph,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3 import MiniMaxH3Attention
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_FP32_DTYPE = torch.float32
|
||||
_BF16_DTYPE = torch.bfloat16
|
||||
|
||||
# one side stream per process and device: under Ulysses the linear readout and
|
||||
# its a2a run on it while FA4 holds the current stream
|
||||
_LINEAR_STREAMS: dict[int, torch.cuda.Stream] = {}
|
||||
|
||||
|
||||
def _linear_branch_stream(device: torch.device) -> torch.cuda.Stream:
|
||||
index = device.index if device.index is not None else torch.cuda.current_device()
|
||||
stream = _LINEAR_STREAMS.get(index)
|
||||
if stream is None:
|
||||
# high priority: the readout's small kernels fill the SM slots FA4's
|
||||
# persistent CTAs leave at their tails
|
||||
stream = torch.cuda.Stream(device=device, priority=-1)
|
||||
_LINEAR_STREAMS[index] = stream
|
||||
return stream
|
||||
|
||||
|
||||
class MiniMaxH3VDNHybridAttention(nn.Module):
|
||||
"""out = to_out(gate_sm * window_softmax(q, k, v)) + to_out_linear(branch(q, k, v))."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
arch: MiniMaxH3DiTArchConfig,
|
||||
quant_config: QuantizationConfig | None,
|
||||
*,
|
||||
prefix: str,
|
||||
local_heads: int,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
hybrid = arch.hybrid_attention
|
||||
self.softmax_gate: VDNSoftmaxGate | None = None
|
||||
if hybrid.enable_softmax_gate:
|
||||
self.softmax_gate = VDNSoftmaxGate(
|
||||
arch.hidden_size,
|
||||
arch.num_attention_heads,
|
||||
prefix=f"{prefix}.softmax_gate",
|
||||
)
|
||||
self.linear_attention = MiniMaxH3VDNLinearBranch(
|
||||
arch, hybrid, local_heads=local_heads, prefix=f"{prefix}.linear_attention"
|
||||
)
|
||||
self.to_out_linear = RowParallelLinear(
|
||||
arch.num_attention_heads * arch.attention_head_dim,
|
||||
arch.hidden_size,
|
||||
bias=False,
|
||||
input_is_parallel=True,
|
||||
params_dtype=_BF16_DTYPE,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.to_out_linear",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build(
|
||||
cls,
|
||||
arch: MiniMaxH3DiTArchConfig,
|
||||
quant_config: QuantizationConfig | None,
|
||||
*,
|
||||
prefix: str,
|
||||
local_heads: int,
|
||||
) -> MiniMaxH3VDNHybridAttention | None:
|
||||
"""None for the dense model and the token refiner (VDN converts the DiT blocks only)."""
|
||||
if arch.hybrid_attention is None or not prefix.startswith("blocks."):
|
||||
return None
|
||||
return cls(
|
||||
arch, quant_config, prefix=f"{prefix}.hybrid", local_heads=local_heads
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
attention: MiniMaxH3Attention,
|
||||
x: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
*,
|
||||
rope_cache: tuple[torch.Tensor, torch.Tensor] | None,
|
||||
cu_seqlens: torch.Tensor,
|
||||
cu_seqlens_host: tuple[int, ...] | None,
|
||||
max_seqlen: int,
|
||||
ulysses_active: bool,
|
||||
ring_active: bool,
|
||||
) -> torch.Tensor:
|
||||
"""out = to_out(gate_sm * window_softmax) + to_out_linear(branch); gates and
|
||||
beta are row-local and computed before the core exchanges rows for heads."""
|
||||
if ring_active:
|
||||
raise NotImplementedError(
|
||||
"VDN-H3 hybrid attention does not support ring parallelism"
|
||||
)
|
||||
total = x.shape[0]
|
||||
softmax_gate = self.softmax_gate(x) if self.softmax_gate is not None else None
|
||||
beta = self.linear_attention.beta(x)
|
||||
gate_hidden, _ = self.linear_attention.output_gate.down(x)
|
||||
attention_core = (
|
||||
_hybrid_attention_core_bcg
|
||||
if attention.bcg_breakpoint
|
||||
else _minimax_h3_hybrid_attention_core_impl
|
||||
)
|
||||
softmax_out, linear_out = attention_core(
|
||||
attention,
|
||||
x,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
softmax_gate,
|
||||
beta,
|
||||
gate_hidden,
|
||||
rope_cache=rope_cache,
|
||||
cu_seqlens=cu_seqlens,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
max_seqlen=max_seqlen,
|
||||
ulysses_active=ulysses_active,
|
||||
)
|
||||
out, _ = attention.out_proj(softmax_out.reshape(total, -1))
|
||||
if linear_out is not None:
|
||||
linear_proj, _ = self.to_out_linear(linear_out)
|
||||
if linear_proj.shape[0] == total:
|
||||
out.add_(linear_proj)
|
||||
else:
|
||||
# single-rank path: the readout covers the video rows only
|
||||
layout = get_forward_context().attn_metadata.layout
|
||||
out[layout.video_start : layout.video_end].add_(linear_proj)
|
||||
return out
|
||||
|
||||
|
||||
def _vdn_frame_partial_sums(
|
||||
x: torch.Tensor,
|
||||
*,
|
||||
row_start: int,
|
||||
video_start: int,
|
||||
video_end: int,
|
||||
num_frames: int,
|
||||
tokens_per_frame: int,
|
||||
) -> torch.Tensor:
|
||||
# fp32 [F, hidden] sums of this rank's video rows; whole frames as one reduction
|
||||
hidden = x.shape[-1]
|
||||
sums = torch.zeros(num_frames, hidden, dtype=_FP32_DTYPE, device=x.device)
|
||||
lo = max(row_start, video_start)
|
||||
hi = min(row_start + x.shape[0], video_end)
|
||||
if lo >= hi:
|
||||
return sums
|
||||
rows = x[lo - row_start : hi - row_start]
|
||||
first_frame, offset = divmod(lo - video_start, tokens_per_frame)
|
||||
lead = (tokens_per_frame - offset) % tokens_per_frame
|
||||
lead = min(lead, rows.shape[0])
|
||||
if lead:
|
||||
sums[first_frame] += rows[:lead].sum(0, dtype=_FP32_DTYPE)
|
||||
first_frame += 1
|
||||
full = (rows.shape[0] - lead) // tokens_per_frame
|
||||
if full:
|
||||
sums[first_frame : first_frame + full] = (
|
||||
rows[lead : lead + full * tokens_per_frame]
|
||||
.view(full, tokens_per_frame, hidden)
|
||||
.sum(1, dtype=_FP32_DTYPE)
|
||||
)
|
||||
tail = lead + full * tokens_per_frame
|
||||
if tail < rows.shape[0]:
|
||||
sums[first_frame + full] += rows[tail:].sum(0, dtype=_FP32_DTYPE)
|
||||
return sums
|
||||
|
||||
|
||||
def _vdn_a2a_rows_to_heads(
|
||||
field: torch.Tensor,
|
||||
*,
|
||||
ulysses_ws: int,
|
||||
role: str,
|
||||
process_group: torch.distributed.ProcessGroup,
|
||||
) -> tuple[torch.distributed.Work, torch.Tensor]:
|
||||
# [L, H, d] row shard -> contiguous [S, H / ws, d] of this rank's heads
|
||||
rows, total_heads, head_dim = field.shape
|
||||
local_heads = total_heads // ulysses_ws
|
||||
send = _a2a_staging_buffer(
|
||||
role + "_send",
|
||||
(ulysses_ws, rows, local_heads, head_dim),
|
||||
field.dtype,
|
||||
field.device,
|
||||
)
|
||||
send.copy_(field.view(rows, ulysses_ws, local_heads, head_dim).permute(1, 0, 2, 3))
|
||||
recv = _a2a_staging_buffer(
|
||||
role + "_recv",
|
||||
(ulysses_ws * rows, local_heads, head_dim),
|
||||
field.dtype,
|
||||
field.device,
|
||||
)
|
||||
work = torch.distributed.all_to_all_single(
|
||||
recv, send, group=process_group, async_op=True
|
||||
)
|
||||
return work, recv
|
||||
|
||||
|
||||
def _vdn_a2a_heads_to_rows(
|
||||
out: torch.Tensor,
|
||||
*,
|
||||
ulysses_ws: int,
|
||||
role: str,
|
||||
process_group: torch.distributed.ProcessGroup,
|
||||
) -> tuple[torch.distributed.Work, torch.Tensor]:
|
||||
# [S, H / ws, d] -> [ws, L, H / ws, d] source-rank major; _vdn_merge_heads after wait
|
||||
seq_len, local_heads, head_dim = out.shape
|
||||
rows = seq_len // ulysses_ws
|
||||
recv = _a2a_staging_buffer(
|
||||
role + "_recv", (ulysses_ws, rows, local_heads, head_dim), out.dtype, out.device
|
||||
)
|
||||
work = torch.distributed.all_to_all_single(
|
||||
recv, out.contiguous(), group=process_group, async_op=True
|
||||
)
|
||||
return work, recv
|
||||
|
||||
|
||||
def _vdn_merge_heads(recv: torch.Tensor) -> torch.Tensor:
|
||||
# [ws, L, h, d] -> [L, ws * h, d]; rank-major heads are the global head order
|
||||
ulysses_ws, rows, local_heads, head_dim = recv.shape
|
||||
merged = usp_merge_heads(recv.view(ulysses_ws, rows, 1, local_heads, head_dim))
|
||||
return merged.reshape(rows, ulysses_ws * local_heads, head_dim)
|
||||
|
||||
|
||||
def _vdn_window_softmax(
|
||||
attention: MiniMaxH3Attention,
|
||||
meta: HybridWindowAttentionH3Metadata,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
*,
|
||||
softmax_gate: torch.Tensor | None,
|
||||
rope_cache: tuple[torch.Tensor, torch.Tensor],
|
||||
cu_seqlens: torch.Tensor,
|
||||
cu_seqlens_host: tuple[int, ...] | None,
|
||||
max_seqlen: int,
|
||||
) -> torch.Tensor:
|
||||
# the branch keeps reading the raw q/k, so norm + RoPE write copies
|
||||
cos_sin_cache, positions = rope_cache
|
||||
if attention._use_fused_qknorm_rope and not torch.compiler.is_compiling():
|
||||
q_sm = torch.empty(q.shape, dtype=q.dtype, device=q.device)
|
||||
k_sm = torch.empty(k.shape, dtype=k.dtype, device=k.device)
|
||||
fused_qknorm_rope_out_of_place(
|
||||
q,
|
||||
k,
|
||||
q_sm,
|
||||
k_sm,
|
||||
attention.q_norm.weight,
|
||||
attention.k_norm.weight,
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
is_neox=True,
|
||||
eps=attention.q_norm.eps,
|
||||
head_dim=attention.head_dim,
|
||||
rope_dim=cos_sin_cache.shape[-1],
|
||||
round_norm_before_rope=True,
|
||||
)
|
||||
else:
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3 import (
|
||||
_apply_qk_norm,
|
||||
_apply_rope_qk,
|
||||
)
|
||||
|
||||
q_sm, k_sm = _apply_qk_norm(
|
||||
q.clone(), k.clone(), attention.q_norm, attention.k_norm, attention.head_dim
|
||||
)
|
||||
q_sm, k_sm = _apply_rope_qk(q_sm, k_sm, cos_sin_cache, positions)
|
||||
return attention._attention_impl.forward_varlen(
|
||||
q_sm,
|
||||
k_sm,
|
||||
v,
|
||||
cu_seqlens=cu_seqlens,
|
||||
max_seqlen=max_seqlen,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
attn_metadata=meta,
|
||||
softmax_gate=softmax_gate,
|
||||
)
|
||||
|
||||
|
||||
def _vdn_linear_readout(
|
||||
attention: MiniMaxH3Attention,
|
||||
meta: HybridWindowAttentionH3Metadata,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
*,
|
||||
beta: torch.Tensor,
|
||||
linear_gate: torch.Tensor,
|
||||
frame_mean: torch.Tensor,
|
||||
head_range: slice | None,
|
||||
) -> torch.Tensor:
|
||||
# video rows in, [V, h * d] out; text rows seed the state
|
||||
layout = meta.layout
|
||||
video = slice(layout.video_start, layout.video_end)
|
||||
text = slice(0, layout.text_len)
|
||||
return attention.hybrid.linear_attention(
|
||||
q_raw=q[video],
|
||||
k_raw=k[video],
|
||||
v_raw=v[video],
|
||||
beta=beta[video],
|
||||
gate=linear_gate[video],
|
||||
frame_mean=frame_mean,
|
||||
layout=layout,
|
||||
text_k_raw=k[text],
|
||||
text_v_raw=v[text],
|
||||
text_beta=beta[text],
|
||||
heads=head_range,
|
||||
)
|
||||
|
||||
|
||||
def _minimax_h3_hybrid_attention_core_impl(
|
||||
attention: MiniMaxH3Attention,
|
||||
x: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
softmax_gate: torch.Tensor | None,
|
||||
beta: torch.Tensor,
|
||||
gate_hidden: torch.Tensor,
|
||||
*,
|
||||
rope_cache: tuple[torch.Tensor, torch.Tensor] | None,
|
||||
cu_seqlens: torch.Tensor,
|
||||
cu_seqlens_host: tuple[int, ...] | None,
|
||||
max_seqlen: int,
|
||||
ulysses_active: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
meta = get_forward_context().attn_metadata
|
||||
if not isinstance(meta, HybridWindowAttentionH3Metadata):
|
||||
raise RuntimeError(
|
||||
"VDN-H3 hybrid attention needs HybridWindowAttentionH3Metadata in the "
|
||||
"forward context; the MiniMax-H3 denoising stage installs it per request "
|
||||
f"(got {type(meta).__name__})."
|
||||
)
|
||||
layout = meta.layout
|
||||
softmax = functools.partial(
|
||||
_vdn_window_softmax,
|
||||
attention,
|
||||
meta,
|
||||
cu_seqlens=cu_seqlens,
|
||||
cu_seqlens_host=cu_seqlens_host,
|
||||
max_seqlen=max_seqlen,
|
||||
)
|
||||
if not ulysses_active:
|
||||
if rope_cache is None:
|
||||
raise RuntimeError("VDN-H3 hybrid attention requires the RoPE cache")
|
||||
softmax_out = softmax(q, k, v, softmax_gate=softmax_gate, rope_cache=rope_cache)
|
||||
if meta.full_cover:
|
||||
return softmax_out, None
|
||||
frame_mean = (
|
||||
x[layout.video_start : layout.video_end]
|
||||
.view(layout.num_frames, layout.tokens_per_frame, x.shape[-1])
|
||||
.mean(dim=1, dtype=_FP32_DTYPE)
|
||||
)
|
||||
readout = _vdn_linear_readout(
|
||||
attention,
|
||||
meta,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
beta=beta,
|
||||
linear_gate=attention.hybrid.linear_attention.output_gate.up_gate(
|
||||
gate_hidden
|
||||
),
|
||||
frame_mean=frame_mean,
|
||||
head_range=None,
|
||||
)
|
||||
return softmax_out, readout
|
||||
|
||||
return _vdn_ulysses_hybrid_core(
|
||||
attention,
|
||||
meta,
|
||||
x,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
softmax_gate=softmax_gate,
|
||||
beta=beta,
|
||||
gate_hidden=gate_hidden,
|
||||
softmax=softmax,
|
||||
)
|
||||
|
||||
|
||||
def _vdn_ulysses_hybrid_core(
|
||||
attention: MiniMaxH3Attention,
|
||||
meta: HybridWindowAttentionH3Metadata,
|
||||
x: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
*,
|
||||
softmax_gate: torch.Tensor | None,
|
||||
beta: torch.Tensor,
|
||||
gate_hidden: torch.Tensor,
|
||||
softmax: Callable[..., torch.Tensor],
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
from sglang.multimodal_gen.runtime.distributed.parallel_state import get_sp_group
|
||||
|
||||
layout = meta.layout
|
||||
if meta.rope_cache_full is None:
|
||||
raise RuntimeError("VDN-H3 under Ulysses needs the full-sequence RoPE cache")
|
||||
sp_group = get_sp_group()
|
||||
process_group = sp_group.ulysses_group
|
||||
ulysses_ws, ulysses_rank = get_ulysses_ctx()
|
||||
local_rows, head_dim = x.shape[0], q.shape[2]
|
||||
local_heads = q.shape[1] // ulysses_ws
|
||||
seq_len = local_rows * ulysses_ws
|
||||
|
||||
# the q/k/v exchange is in flight while the frame sums and the gate hidden go out
|
||||
inflight = [
|
||||
_vdn_a2a_rows_to_heads(
|
||||
field,
|
||||
ulysses_ws=ulysses_ws,
|
||||
role=f"vdn_{name}",
|
||||
process_group=process_group,
|
||||
)
|
||||
for name, field in (("q", q), ("k", k), ("v", v))
|
||||
]
|
||||
frame_sums = _vdn_frame_partial_sums(
|
||||
x,
|
||||
row_start=ulysses_rank * local_rows,
|
||||
video_start=layout.video_start,
|
||||
video_end=layout.video_end,
|
||||
num_frames=layout.num_frames,
|
||||
tokens_per_frame=layout.tokens_per_frame,
|
||||
)
|
||||
frame_work = torch.distributed.all_reduce(
|
||||
frame_sums, group=sp_group.device_group, async_op=True
|
||||
)
|
||||
# the per-head scalars (beta, softmax gate) ride one more async field
|
||||
scalars = [beta] if softmax_gate is None else [beta, softmax_gate]
|
||||
inflight.append(
|
||||
_vdn_a2a_rows_to_heads(
|
||||
torch.stack(scalars, dim=-1),
|
||||
ulysses_ws=ulysses_ws,
|
||||
role="vdn_scalars",
|
||||
process_group=process_group,
|
||||
)
|
||||
)
|
||||
head_range = slice(ulysses_rank * local_heads, (ulysses_rank + 1) * local_heads)
|
||||
linear_gate = attention.hybrid.linear_attention.output_gate.up_gate(
|
||||
sp_group.all_gather(gate_hidden.contiguous(), dim=0), heads=head_range
|
||||
)
|
||||
|
||||
for work, _ in inflight:
|
||||
work.wait()
|
||||
q, k, v, scalars = (recv for _, recv in inflight)
|
||||
beta = scalars[..., 0]
|
||||
if softmax_gate is not None:
|
||||
softmax_gate = scalars[..., 1]
|
||||
softmax = functools.partial(
|
||||
softmax, q, k, v, softmax_gate=softmax_gate, rope_cache=meta.rope_cache_full
|
||||
)
|
||||
if meta.full_cover:
|
||||
softmax_out = softmax()
|
||||
frame_work.wait()
|
||||
return _vdn_return_to_rows(
|
||||
softmax_out, None, ulysses_ws=ulysses_ws, process_group=process_group
|
||||
)
|
||||
|
||||
def linear_branch() -> torch.Tensor:
|
||||
frame_work.wait()
|
||||
readout = _vdn_linear_readout(
|
||||
attention,
|
||||
meta,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
beta=beta,
|
||||
linear_gate=linear_gate,
|
||||
frame_mean=frame_sums / layout.tokens_per_frame,
|
||||
head_range=head_range,
|
||||
)
|
||||
# rows go back to their owners: pad the non-video rows with zeros
|
||||
linear_out = q.new_zeros(seq_len, local_heads, head_dim)
|
||||
linear_out[layout.video_start : layout.video_end] = readout.view(
|
||||
-1, local_heads, head_dim
|
||||
)
|
||||
return linear_out
|
||||
|
||||
a2a_back = functools.partial(
|
||||
_vdn_a2a_heads_to_rows, ulysses_ws=ulysses_ws, process_group=process_group
|
||||
)
|
||||
# the linear readout and its a2a run on the side stream while FA4 (issued
|
||||
# first) holds the current one; both read the exchanged q/k/v, frame sums and
|
||||
# gate, so the side stream waits for them and the current stream joins before the merge
|
||||
main_stream = torch.cuda.current_stream(q.device)
|
||||
side_stream = _linear_branch_stream(q.device)
|
||||
side_stream.wait_stream(main_stream)
|
||||
softmax_out = softmax()
|
||||
softmax_work, softmax_recv = a2a_back(softmax_out, role="vdn_out0")
|
||||
with torch.cuda.stream(side_stream):
|
||||
linear_work, linear_recv = a2a_back(linear_branch(), role="vdn_out1")
|
||||
main_stream.wait_stream(side_stream)
|
||||
softmax_work.wait()
|
||||
linear_work.wait()
|
||||
merged_softmax = _vdn_merge_heads(softmax_recv)
|
||||
merged_linear = _vdn_merge_heads(linear_recv)
|
||||
return merged_softmax, merged_linear.reshape(merged_linear.shape[0], -1)
|
||||
|
||||
|
||||
def _vdn_return_to_rows(
|
||||
softmax_out: torch.Tensor,
|
||||
linear_out: torch.Tensor | None,
|
||||
*,
|
||||
ulysses_ws: int,
|
||||
process_group: torch.distributed.ProcessGroup,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
# [S, h, d] per branch -> ([L, H, d], [L, H * d] or None), both trips in flight together
|
||||
branch_outputs = [out for out in (softmax_out, linear_out) if out is not None]
|
||||
inflight = [
|
||||
_vdn_a2a_heads_to_rows(
|
||||
out, ulysses_ws=ulysses_ws, role=f"vdn_out{i}", process_group=process_group
|
||||
)
|
||||
for i, out in enumerate(branch_outputs)
|
||||
]
|
||||
merged = []
|
||||
for work, recv in inflight:
|
||||
work.wait()
|
||||
merged.append(_vdn_merge_heads(recv))
|
||||
linear_rows = (
|
||||
merged[1].reshape(merged[1].shape[0], -1) if linear_out is not None else None
|
||||
)
|
||||
return merged[0], linear_rows
|
||||
|
||||
|
||||
_hybrid_attention_core_bcg = eager_on_graph(True)(
|
||||
_minimax_h3_hybrid_attention_core_impl
|
||||
)
|
||||
|
||||
|
||||
def prepare_hybrid_attention_metadata(
|
||||
*,
|
||||
model,
|
||||
packed: Mapping[str, torch.Tensor],
|
||||
latent_shape: tuple[int, int, int],
|
||||
server_args,
|
||||
device: torch.device,
|
||||
) -> Callable[[int], Any] | None:
|
||||
"""Request-static metadata (window plan, packed layout, full-sequence RoPE
|
||||
cache under Ulysses) for every step and block; None for other backends."""
|
||||
model._resolve_attention_backend_once()
|
||||
if (
|
||||
model._resolved_attention_backend
|
||||
is not AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3
|
||||
):
|
||||
return None
|
||||
hybrid = model.arch.hybrid_attention
|
||||
if hybrid is None:
|
||||
raise ValueError(
|
||||
"--attention-backend hybrid_window_attn_h3 needs a VDN-H3 checkpoint "
|
||||
"(transformer/config.json with hybrid_attention); this checkpoint has "
|
||||
"no linear branch. Use --attention-backend fa for MiniMax-H3."
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3 import (
|
||||
_rope_cos_sin_cache,
|
||||
)
|
||||
|
||||
config = server_args.attention_backend_config or {}
|
||||
max_gather_rows = int(config.get("vdn_max_gather_rows", 200_000))
|
||||
latent_t, latent_h, latent_w = latent_shape
|
||||
layout = vdn_h3_layout_from_packed(
|
||||
packed, latent_t=latent_t, latent_h=latent_h, latent_w=latent_w
|
||||
)
|
||||
rope_cache_full = None
|
||||
ulysses_ws, _ = get_ulysses_ctx()
|
||||
ring_ws, _ = get_ring_ctx()
|
||||
if ring_ws > 1:
|
||||
raise ValueError("VDN-H3 does not support ring parallelism")
|
||||
if ulysses_ws > 1:
|
||||
# QK-norm + RoPE run on the head shard after the all-to-all: full-sequence cache
|
||||
with torch.inference_mode():
|
||||
img_position_ids = (
|
||||
packed["img_position_ids"][None].to(torch.float32).to(device)
|
||||
)
|
||||
rope_freqs = model.rope(img_position_ids)
|
||||
rope_cache_full = (
|
||||
_rope_cos_sin_cache(rope_freqs, dtype=torch.bfloat16),
|
||||
torch.arange(layout.seq_len, device=device, dtype=torch.long),
|
||||
)
|
||||
metadata = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout,
|
||||
hybrid=hybrid,
|
||||
device=device,
|
||||
rope_cache_full=rope_cache_full,
|
||||
max_gather_rows=max_gather_rows,
|
||||
)
|
||||
logger.info(
|
||||
"VDN-H3 hybrid attention: frames=%d tokens/frame=%d text=%d "
|
||||
"used=%d/%d chunk=%d radius=%d anchors=%s full_cover=%s",
|
||||
layout.num_frames,
|
||||
layout.tokens_per_frame,
|
||||
layout.text_len,
|
||||
layout.used,
|
||||
layout.seq_len,
|
||||
hybrid.chunk,
|
||||
hybrid.radius,
|
||||
hybrid.anchor_frames,
|
||||
metadata.full_cover,
|
||||
)
|
||||
|
||||
def build(step_index: int):
|
||||
return metadata
|
||||
|
||||
return build
|
||||
|
||||
|
||||
__all__ = ["MiniMaxH3VDNHybridAttention", "prepare_hybrid_attention_metadata"]
|
||||
@@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from sglang.multimodal_gen.runtime.pipelines.minimax_h3_pipeline import (
|
||||
MiniMaxH3Pipeline,
|
||||
)
|
||||
|
||||
|
||||
class VDNH3Pipeline(MiniMaxH3Pipeline):
|
||||
"""VDN-H3 on the MiniMax-H3 pipeline: the model overlay materializes a
|
||||
base-H3 layout (LoRAs prefused, linear branch attached), so only the DiT
|
||||
blocks' attention and its backend differ."""
|
||||
|
||||
pipeline_name = "VDNH3Pipeline"
|
||||
default_model_subfolder = None
|
||||
|
||||
|
||||
EntryClass = [VDNH3Pipeline]
|
||||
+12
@@ -749,6 +749,18 @@ class MiniMaxH3DenoisingStage(DenoisingStage):
|
||||
server_args=server_args,
|
||||
device=device,
|
||||
)
|
||||
if build_vsa_h3_step_metadata is None:
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn_attention import (
|
||||
prepare_hybrid_attention_metadata,
|
||||
)
|
||||
|
||||
build_vsa_h3_step_metadata = prepare_hybrid_attention_metadata(
|
||||
model=model,
|
||||
packed=packed,
|
||||
latent_shape=(ctx.latent_t, ctx.latent_h, ctx.latent_w),
|
||||
server_args=server_args,
|
||||
device=device,
|
||||
)
|
||||
positive = MiniMaxH3DenoiseBranch(
|
||||
packed=packed,
|
||||
text_embeddings=emb["hidden_states"],
|
||||
|
||||
@@ -290,6 +290,52 @@ class _VideoSparseAttentionH3BackendResolver(_CudaAttentionBackendResolver):
|
||||
) from e
|
||||
|
||||
|
||||
class _HybridWindowAttentionH3BackendResolver(_CudaAttentionBackendResolver):
|
||||
backend = AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3
|
||||
|
||||
# the window rides FlashAttention varlen: FA4 on SM100 / SM103 / SM120, FA3 on
|
||||
# SM90; SM80 / SM86 / SM89 run FA3's Sm80 mainloop (FA2-class throughput)
|
||||
supported_capabilities = {
|
||||
(8, 0),
|
||||
(8, 6),
|
||||
(8, 9),
|
||||
(9, 0),
|
||||
(10, 0),
|
||||
(10, 3),
|
||||
(12, 0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, platform) -> str:
|
||||
capability = platform.get_device_capability()
|
||||
capability_tuple = (
|
||||
(capability.major, capability.minor) if capability is not None else None
|
||||
)
|
||||
if capability_tuple not in cls.supported_capabilities:
|
||||
found = capability.as_version_str() if capability else "unknown"
|
||||
raise ValueError(
|
||||
"hybrid_window_attn_h3 (VDN-H3) needs compute capability 8.0 / "
|
||||
"8.6 / 8.9 (Ampere, Ada), 9.0 (Hopper), 10.0 (B200 / GB200), "
|
||||
"10.3 (B300 / GB300) or 12.0 (RTX PRO 6000 Blackwell); this "
|
||||
f"device reports {found}."
|
||||
)
|
||||
if not platform._prepare_flash_attention_for_blackwell():
|
||||
raise RuntimeError(
|
||||
"hybrid_window_attn_h3 requires FlashAttention for its dense legs"
|
||||
)
|
||||
try:
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3 import ( # noqa: F401
|
||||
HybridWindowAttentionH3Backend,
|
||||
)
|
||||
|
||||
return "sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3.HybridWindowAttentionH3Backend"
|
||||
except Exception as e:
|
||||
logger.error("Failed to import hybrid_window_attn_h3 backend: %s", str(e))
|
||||
raise ImportError(
|
||||
"hybrid_window_attn_h3 needs FlashAttention and Triton."
|
||||
) from e
|
||||
|
||||
|
||||
class _CubeSparseAttentionBackendResolver(_CudaAttentionBackendResolver):
|
||||
backend = AttentionBackendEnum.CUBE_SPARSE_ATTN
|
||||
|
||||
@@ -485,6 +531,7 @@ _CUDA_ATTENTION_BACKEND_RESOLVERS = {
|
||||
_SpargeAttentionBackendResolver,
|
||||
_VideoSparseAttentionBackendResolver,
|
||||
_VideoSparseAttentionH3BackendResolver,
|
||||
_HybridWindowAttentionH3BackendResolver,
|
||||
_CubeSparseAttentionBackendResolver,
|
||||
_SparseVideoGen2AttentionBackendResolver,
|
||||
_SolAttnBackendResolver,
|
||||
@@ -652,7 +699,9 @@ class CudaPlatformBase(Platform):
|
||||
|
||||
@classmethod
|
||||
def _prepare_flash_attention_for_blackwell(cls) -> bool:
|
||||
if not cls.is_blackwell():
|
||||
# the FA4 CuTe package ships an sm120 forward kernel; the default FA backend
|
||||
# still resolves to SDPA on SM120 before reaching this
|
||||
if not (cls.is_blackwell() or cls.is_sm120()):
|
||||
return True
|
||||
|
||||
try:
|
||||
|
||||
@@ -36,6 +36,7 @@ class AttentionBackendEnum(enum.Enum):
|
||||
SPARGE_ATTN = enum.auto()
|
||||
VIDEO_SPARSE_ATTN = enum.auto()
|
||||
VIDEO_SPARSE_ATTN_H3 = enum.auto()
|
||||
HYBRID_WINDOW_ATTN_H3 = enum.auto()
|
||||
SPARSE_VIDEO_GEN_2_ATTN = enum.auto()
|
||||
VMOBA_ATTN = enum.auto()
|
||||
AITER = enum.auto()
|
||||
@@ -59,6 +60,7 @@ class AttentionBackendEnum(enum.Enum):
|
||||
AttentionBackendEnum.SLIDING_TILE_ATTN,
|
||||
AttentionBackendEnum.VIDEO_SPARSE_ATTN,
|
||||
AttentionBackendEnum.VIDEO_SPARSE_ATTN_H3,
|
||||
AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3,
|
||||
AttentionBackendEnum.SPARSE_VIDEO_GEN_2_ATTN,
|
||||
AttentionBackendEnum.VMOBA_ATTN,
|
||||
AttentionBackendEnum.SLA_ATTN,
|
||||
|
||||
@@ -46,6 +46,10 @@ BUILTIN_MODEL_OVERLAY_REGISTRY: dict[str, dict[str, Any]] = {
|
||||
"overlay_repo_id": "kevin-mi/FastH3-4step-Preview-overlay",
|
||||
"overlay_revision": "f769cb8001dae335089de7b250364335bc7cb183",
|
||||
},
|
||||
"OpenVDN/vdn-minimax-h3": {
|
||||
"overlay_repo_id": "kevin-mi/VDN-H3-overlay",
|
||||
"overlay_revision": "0ad315a05b914c4003af4d26152d288c2506a609",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -688,6 +688,47 @@ MINIMAX_H3_FOUR_GPU_H100_CASES = [
|
||||
run_models_api_check=False,
|
||||
run_t2v_input_reference_check=False,
|
||||
),
|
||||
DiffusionTestCase(
|
||||
"vdn_h3_t2va_4gpu_h100",
|
||||
DiffusionServerArgs(
|
||||
model_path="OpenVDN/vdn-minimax-h3",
|
||||
modality="video",
|
||||
num_gpus=4,
|
||||
extras=[
|
||||
"--attention-backend",
|
||||
"hybrid_window_attn_h3",
|
||||
"--enable-torch-compile",
|
||||
"false",
|
||||
],
|
||||
),
|
||||
DiffusionSamplingParams(
|
||||
prompt=(
|
||||
"A curious raccoon peers through a vibrant field of yellow "
|
||||
"sunflowers, its eyes wide with interest."
|
||||
),
|
||||
output_size="1344x768",
|
||||
seconds=5,
|
||||
output_format="mp4",
|
||||
expect_audio_output=True,
|
||||
num_outputs_per_prompt=1,
|
||||
extras={
|
||||
"task": "t2va",
|
||||
"conditions": [],
|
||||
"target": {
|
||||
"short_edge": 768,
|
||||
"aspect_ratio": "16:9",
|
||||
"duration_seconds": 5.0,
|
||||
},
|
||||
"num_inference_steps": 9,
|
||||
"seed": 42,
|
||||
},
|
||||
),
|
||||
run_perf_check=False,
|
||||
run_consistency_check=False,
|
||||
run_component_accuracy_check=False,
|
||||
run_models_api_check=False,
|
||||
run_t2v_input_reference_check=False,
|
||||
),
|
||||
DiffusionTestCase(
|
||||
"fasth3_t2va_vsa_4gpu_h100",
|
||||
DiffusionServerArgs(
|
||||
@@ -1428,6 +1469,7 @@ STANDALONE_FILES = {
|
||||
"../single_test_file/test_dp_serving_2_gpu.py",
|
||||
"../single_test_file/test_pynccl_a2a_capture_2_gpu.py",
|
||||
"../single_test_file/test_usp_replicated_parity_2_gpu.py",
|
||||
"../single_test_file/test_vdn_ulysses_exchange_2_gpu.py",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1473,6 +1515,8 @@ STANDALONE_FILE_EST_TIMES = {
|
||||
"../single_test_file/test_pynccl_a2a_capture_2_gpu.py": 180.0,
|
||||
# two SDPA parity checks on 128+6 rows
|
||||
"../single_test_file/test_usp_replicated_parity_2_gpu.py": 180.0,
|
||||
# no model load; two small all-to-alls
|
||||
"../single_test_file/test_vdn_ulysses_exchange_2_gpu.py": 60.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 Ulysses exchange: the field-major row->head all-to-all and its
|
||||
inverse plus the head merge, checked against plain slicing on 2 GPUs."""
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from sglang.test.test_utils import run_distributed_test
|
||||
|
||||
|
||||
def _check(rank: int) -> None:
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn_attention import (
|
||||
_vdn_a2a_heads_to_rows,
|
||||
_vdn_a2a_rows_to_heads,
|
||||
_vdn_merge_heads,
|
||||
)
|
||||
|
||||
world = dist.get_world_size()
|
||||
device = torch.device("cuda", rank)
|
||||
heads, head_dim, local_rows = 6, 32, 24
|
||||
local_heads = heads // world
|
||||
seq = local_rows * world
|
||||
# every rank builds the same global tensors, so a shard is checked by slicing
|
||||
g = torch.Generator(device="cpu").manual_seed(3)
|
||||
qkv = torch.randn(seq, 3 * heads * head_dim, generator=g).to(device, torch.bfloat16)
|
||||
q = qkv.view(seq, 3, heads, head_dim)[:, 0] # strided, as the split qkv is
|
||||
scalars = torch.randn(seq, heads, 2, generator=g).to(device, torch.bfloat16)
|
||||
rows = slice(rank * local_rows, (rank + 1) * local_rows)
|
||||
mine = slice(rank * local_heads, (rank + 1) * local_heads)
|
||||
with torch.inference_mode():
|
||||
for name, field in (("q", q), ("scalars", scalars)):
|
||||
work, recv = _vdn_a2a_rows_to_heads(
|
||||
field[rows], ulysses_ws=world, role=name, process_group=dist.group.WORLD
|
||||
)
|
||||
work.wait()
|
||||
assert recv.is_contiguous()
|
||||
assert torch.equal(recv, field[:, mine])
|
||||
# inverse: this rank's heads for every row -> row shard, every head
|
||||
work, back = _vdn_a2a_heads_to_rows(
|
||||
q[:, mine].contiguous() * 2,
|
||||
ulysses_ws=world,
|
||||
role="out",
|
||||
process_group=dist.group.WORLD,
|
||||
)
|
||||
work.wait()
|
||||
assert torch.equal(_vdn_merge_heads(back), q[rows] * 2)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
|
||||
def test_exchange_round_trip_two_ranks() -> None:
|
||||
run_distributed_test(_check, world_size=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_exchange_round_trip_two_ranks()
|
||||
@@ -107,6 +107,51 @@ class TestCudaAttentionBackendSelection(unittest.TestCase):
|
||||
|
||||
fake_flash_attn.set_fa_ver.assert_called_once_with(4)
|
||||
|
||||
def test_hybrid_window_h3_on_sm120_uses_fa4(self):
|
||||
FakeCudaPlatform.is_sm120_device = True
|
||||
FakeCudaPlatform.device_capability = DeviceCapability(12, 0)
|
||||
fa_module = "sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn"
|
||||
fake_flash_attn = ModuleType(fa_module)
|
||||
fake_flash_attn.set_fa_ver = Mock()
|
||||
hybrid_module = "sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3"
|
||||
fake_hybrid = ModuleType(hybrid_module)
|
||||
fake_hybrid.HybridWindowAttentionH3Backend = object
|
||||
|
||||
with patch.dict(
|
||||
"sys.modules", {fa_module: fake_flash_attn, hybrid_module: fake_hybrid}
|
||||
):
|
||||
self.assertEqual(
|
||||
self.resolve(AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3),
|
||||
f"{hybrid_module}.HybridWindowAttentionH3Backend",
|
||||
)
|
||||
|
||||
fake_flash_attn.set_fa_ver.assert_called_once_with(4)
|
||||
|
||||
def test_hybrid_window_h3_on_ampere_keeps_fa3(self):
|
||||
FakeCudaPlatform.device_capability = DeviceCapability(8, 0)
|
||||
fa_module = "sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn"
|
||||
fake_flash_attn = ModuleType(fa_module)
|
||||
fake_flash_attn.set_fa_ver = Mock()
|
||||
hybrid_module = "sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3"
|
||||
fake_hybrid = ModuleType(hybrid_module)
|
||||
fake_hybrid.HybridWindowAttentionH3Backend = object
|
||||
|
||||
with patch.dict(
|
||||
"sys.modules", {fa_module: fake_flash_attn, hybrid_module: fake_hybrid}
|
||||
):
|
||||
self.assertEqual(
|
||||
self.resolve(AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3),
|
||||
f"{hybrid_module}.HybridWindowAttentionH3Backend",
|
||||
)
|
||||
|
||||
fake_flash_attn.set_fa_ver.assert_not_called()
|
||||
|
||||
def test_hybrid_window_h3_rejects_unsupported_capability(self):
|
||||
FakeCudaPlatform.device_capability = DeviceCapability(7, 5)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "12.0"):
|
||||
self.resolve(AttentionBackendEnum.HYBRID_WINDOW_ATTN_H3)
|
||||
|
||||
def test_default_backend_uses_torch_sdpa_on_sm120(self):
|
||||
FakeCudaPlatform.is_sm120_device = True
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""hybrid_window_attn_h3 must reproduce a masked dense softmax with exactly the VDN
|
||||
mask on a ragged packed layout, and dense attention once the window covers the clip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3_vdn import (
|
||||
VDNHybridAttentionArchConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.hybrid_window_attn_h3 import (
|
||||
HybridWindowAttentionH3Impl,
|
||||
HybridWindowAttentionH3MetadataBuilder,
|
||||
window_mask_frames,
|
||||
window_mask_reference,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn import VDNH3Layout
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
|
||||
# torch.cuda.is_available() is also True under ROCm, but the backend lives on the
|
||||
# CUDA platform only (RocmPlatform rejects hybrid_window_attn_h3 outright and has
|
||||
# no _prepare_flash_attention_for_blackwell), so gate on the platform itself.
|
||||
requires_cuda = pytest.mark.skipif(
|
||||
not current_platform.is_cuda(),
|
||||
reason="hybrid_window_attn_h3 kernels need NVIDIA CUDA",
|
||||
)
|
||||
|
||||
# ragged on purpose: 70 and 100 are not tile multiples, 12 frames is not a chunk multiple
|
||||
TEXT_LEN = 70
|
||||
AUDIO_ROWS = 100
|
||||
NUM_FRAMES = 12
|
||||
FRAME_H, FRAME_W = 6, 8
|
||||
TOKENS_PER_FRAME = FRAME_H * FRAME_W
|
||||
HEADS = 4
|
||||
HEAD_DIM = 128
|
||||
|
||||
|
||||
def _layout() -> VDNH3Layout:
|
||||
video_start = TEXT_LEN + AUDIO_ROWS
|
||||
used = video_start + NUM_FRAMES * TOKENS_PER_FRAME
|
||||
seq_len = (used + 63) // 64 * 64
|
||||
return VDNH3Layout(
|
||||
seq_len=seq_len,
|
||||
used=used,
|
||||
text_len=TEXT_LEN,
|
||||
video_start=video_start,
|
||||
num_frames=NUM_FRAMES,
|
||||
tokens_per_frame=TOKENS_PER_FRAME,
|
||||
frame_height=FRAME_H,
|
||||
frame_width=FRAME_W,
|
||||
)
|
||||
|
||||
|
||||
def _hybrid(**overrides) -> VDNHybridAttentionArchConfig:
|
||||
kwargs = dict(chunk=5, radius=1, anchor_frames="both")
|
||||
kwargs.update(overrides)
|
||||
return VDNHybridAttentionArchConfig(**kwargs)
|
||||
|
||||
|
||||
def _qkv(device, seed: int = 7):
|
||||
layout = _layout()
|
||||
generator = torch.Generator(device="cpu").manual_seed(seed)
|
||||
tensors = [
|
||||
torch.randn(
|
||||
(layout.seq_len, HEADS, HEAD_DIM), generator=generator, dtype=torch.float32
|
||||
).to(device=device, dtype=torch.bfloat16)
|
||||
for _ in range(3)
|
||||
]
|
||||
return layout, tensors
|
||||
|
||||
|
||||
def _masked_reference(q, k, v, mask: torch.Tensor, used: int) -> torch.Tensor:
|
||||
qf = q[:used].float().permute(1, 0, 2)
|
||||
kf = k[:used].float().permute(1, 0, 2)
|
||||
vf = v[:used].float().permute(1, 0, 2)
|
||||
scores = qf @ kf.transpose(-2, -1) / math.sqrt(HEAD_DIM)
|
||||
scores = scores.masked_fill(~mask[None], float("-inf"))
|
||||
return (torch.softmax(scores, dim=-1) @ vf).permute(1, 0, 2)
|
||||
|
||||
|
||||
def _prepare_flash_attention() -> None:
|
||||
# the platform resolver runs this before the first forward; a direct impl must too
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
|
||||
current_platform._prepare_flash_attention_for_blackwell()
|
||||
|
||||
|
||||
def _impl() -> HybridWindowAttentionH3Impl:
|
||||
_prepare_flash_attention()
|
||||
impl = HybridWindowAttentionH3Impl(
|
||||
num_heads=HEADS,
|
||||
head_size=HEAD_DIM,
|
||||
causal=False,
|
||||
softmax_scale=HEAD_DIM**-0.5,
|
||||
num_kv_heads=HEADS,
|
||||
prefix="blocks.3.attn",
|
||||
)
|
||||
assert impl.layer_idx == 3
|
||||
return impl
|
||||
|
||||
|
||||
def _run(impl, meta, layout, q, k, v, gate=None):
|
||||
cu = torch.tensor(
|
||||
[0, layout.used, layout.seq_len], dtype=torch.int32, device=q.device
|
||||
)
|
||||
return impl.forward_varlen(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens=cu,
|
||||
max_seqlen=layout.used,
|
||||
cu_seqlens_host=(0, layout.used, layout.seq_len),
|
||||
attn_metadata=meta,
|
||||
softmax_gate=gate,
|
||||
)
|
||||
|
||||
|
||||
def test_window_bounds_and_anchor_frames() -> None:
|
||||
hybrid = _hybrid()
|
||||
bounds, dense_rows, dense_cols = window_mask_frames(hybrid, NUM_FRAMES)
|
||||
# chunk 5, radius 1: frame 7 (chunk 1) sees chunks 0..2 = frames 0..14 -> clamped 11
|
||||
assert bounds[7] == (0, 11)
|
||||
assert bounds[0] == (0, 9)
|
||||
assert bounds[11] == (5, 11)
|
||||
assert dense_rows == {0, NUM_FRAMES - 1} and dense_cols == {0, NUM_FRAMES - 1}
|
||||
assert not hybrid.full_cover(NUM_FRAMES)
|
||||
assert _hybrid(radius=NUM_FRAMES).full_cover(NUM_FRAMES)
|
||||
# 102 = 20 * 5 + 2: the last chunk is short but still a whole chunk
|
||||
raw = hybrid.window_bounds(102)
|
||||
assert raw[100] == raw[101] == (95, 109)
|
||||
assert raw[99] == (90, 104)
|
||||
|
||||
|
||||
def test_mask_reference_partition_is_exact() -> None:
|
||||
"""Every (video q, video k) pair is in the softmax window or in the
|
||||
linear branch's complement exactly once; anchors are absent from the
|
||||
branch."""
|
||||
layout = _layout()
|
||||
hybrid = _hybrid()
|
||||
mask = window_mask_reference(hybrid, layout, torch.device("cpu"))
|
||||
# globals dense both ways
|
||||
assert mask[: layout.video_start].all() and mask[:, : layout.video_start].all()
|
||||
assert mask[layout.video_end : layout.used].all()
|
||||
bounds = hybrid.window_bounds(NUM_FRAMES)
|
||||
vs, tpf = layout.video_start, TOKENS_PER_FRAME
|
||||
for qf in range(NUM_FRAMES):
|
||||
row = mask[vs + qf * tpf, vs : layout.video_end].view(NUM_FRAMES, tpf)
|
||||
frame_kept = row.all(dim=1)
|
||||
assert (frame_kept == row.any(dim=1)).all() # whole frames
|
||||
if qf in (0, NUM_FRAMES - 1):
|
||||
assert frame_kept.all()
|
||||
continue
|
||||
lo, hi = max(bounds[qf][0], 0), min(bounds[qf][1], NUM_FRAMES - 1)
|
||||
expected_softmax = {f for f in range(lo, hi + 1)} | {0, NUM_FRAMES - 1}
|
||||
# the branch covers the inner frames 1..F-2 outside the window
|
||||
expected_linear = {f for f in range(1, NUM_FRAMES - 1) if f < lo or f > hi}
|
||||
kept = {f for f in range(NUM_FRAMES) if frame_kept[f]}
|
||||
assert kept == expected_softmax
|
||||
assert kept.isdisjoint(expected_linear)
|
||||
assert kept | expected_linear == set(range(NUM_FRAMES))
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_window_matches_masked_dense() -> None:
|
||||
device = torch.device("cuda")
|
||||
layout, (q, k, v) = _qkv(device)
|
||||
hybrid = _hybrid()
|
||||
meta = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout, hybrid=hybrid, device=device
|
||||
)
|
||||
assert not meta.full_cover
|
||||
out = _run(_impl(), meta, layout, q, k, v)
|
||||
mask = window_mask_reference(hybrid, layout, device)
|
||||
reference = _masked_reference(q, k, v, mask, layout.used)
|
||||
diff = (out[: layout.used].float() - reference).abs().max().item()
|
||||
assert diff < 2e-2, f"window vs masked dense max diff {diff}"
|
||||
assert torch.all(out[layout.used :] == 0)
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_full_cover_matches_dense() -> None:
|
||||
device = torch.device("cuda")
|
||||
layout, (q, k, v) = _qkv(device, seed=11)
|
||||
hybrid = _hybrid(radius=NUM_FRAMES)
|
||||
meta = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout, hybrid=hybrid, device=device
|
||||
)
|
||||
assert meta.full_cover
|
||||
out = _run(_impl(), meta, layout, q, k, v)
|
||||
full = torch.ones(layout.used, layout.used, dtype=torch.bool, device=device)
|
||||
reference = _masked_reference(q, k, v, full, layout.used)
|
||||
diff = (out[: layout.used].float() - reference).abs().max().item()
|
||||
assert diff < 2e-2, f"full cover vs dense max diff {diff}"
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_decomposed_passes_are_arithmetic_neutral() -> None:
|
||||
"""Bounding the gathered K/V rows per pass splits the window into several
|
||||
varlen calls without changing any query's kept set."""
|
||||
device = torch.device("cuda")
|
||||
layout, (q, k, v) = _qkv(device, seed=9)
|
||||
one = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout, hybrid=_hybrid(), device=device
|
||||
)
|
||||
many = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout, hybrid=_hybrid(), device=device, max_gather_rows=1
|
||||
)
|
||||
assert len(one.decomposed.passes) == 1 and len(many.decomposed.passes) > 1
|
||||
impl = _impl()
|
||||
a = _run(impl, one, layout, q, k, v).clone()
|
||||
b = _run(impl, many, layout, q, k, v)
|
||||
assert torch.equal(a, b)
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_dense_fallback_off_the_dit_blocks() -> None:
|
||||
"""The token refiner resolves the same backend but runs plain dense FA."""
|
||||
device = torch.device("cuda")
|
||||
layout, (q, k, v) = _qkv(device, seed=5)
|
||||
_prepare_flash_attention()
|
||||
impl = HybridWindowAttentionH3Impl(
|
||||
num_heads=HEADS,
|
||||
head_size=HEAD_DIM,
|
||||
causal=False,
|
||||
softmax_scale=HEAD_DIM**-0.5,
|
||||
num_kv_heads=HEADS,
|
||||
prefix="token_refiner.blocks.0.attn",
|
||||
)
|
||||
assert impl.layer_idx is None
|
||||
out = _run(impl, None, layout, q, k, v)
|
||||
full = torch.ones(layout.used, layout.used, dtype=torch.bool, device=device)
|
||||
reference = _masked_reference(q, k, v, full, layout.used)
|
||||
assert (out[: layout.used].float() - reference).abs().max().item() < 2e-2
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_metadata_layout_mismatch_is_rejected() -> None:
|
||||
device = torch.device("cuda")
|
||||
layout, (q, k, v) = _qkv(device)
|
||||
meta = HybridWindowAttentionH3MetadataBuilder().build(
|
||||
layout=layout, hybrid=_hybrid(), device=device
|
||||
)
|
||||
cu = torch.tensor(
|
||||
[0, layout.used - 48, layout.seq_len], dtype=torch.int32, device=device
|
||||
)
|
||||
with pytest.raises(ValueError, match="diverged"):
|
||||
_impl().forward_varlen(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
cu_seqlens=cu,
|
||||
max_seqlen=layout.used - 48,
|
||||
cu_seqlens_host=(0, layout.used - 48, layout.seq_len),
|
||||
attn_metadata=meta,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -238,7 +238,9 @@ def test_cache_dit_preservation_only_makes_first_gate_out_of_place():
|
||||
block.norm1 = torch.nn.Identity()
|
||||
block.norm2 = torch.nn.Identity()
|
||||
block.attn = _KwargIdentity()
|
||||
block.attn.qkv_proj = SimpleNamespace(quant_method=UnquantizedLinearMethod())
|
||||
block.mlp = torch.nn.Identity()
|
||||
block.mlp.fc1 = SimpleNamespace(quant_method=UnquantizedLinearMethod())
|
||||
gate_modes = []
|
||||
|
||||
def fake_gate(residual, _gate, _other, _indices, *, dtype, allow_inplace=True):
|
||||
|
||||
@@ -0,0 +1,703 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""VDN-H3 (hybrid window-softmax + Video Delta linear attention MiniMax-H3):
|
||||
registration, admission, and the linear branch's arithmetic against
|
||||
step-by-step references (no weights, CPU + small CUDA shapes)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3 import (
|
||||
MiniMaxH3DiTArchConfig,
|
||||
MiniMaxH3DiTConfig,
|
||||
VDNHybridAttentionArchConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import (
|
||||
MiniMaxH3PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3_vdn import (
|
||||
VDNH3PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.sample.minimax_h3_vdn import VDNH3SamplingParams
|
||||
from sglang.multimodal_gen.registry import (
|
||||
get_model_info,
|
||||
get_non_diffusers_pipeline_name,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn import (
|
||||
TEXT_STATE_SCALE,
|
||||
MiniMaxH3VDNLinearBranch,
|
||||
VDNH3Layout,
|
||||
delta_factor_apply,
|
||||
frame_statistics,
|
||||
gather_linear_state,
|
||||
run_scans,
|
||||
vdn_h3_layout_from_packed,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import (
|
||||
AttentionBackendEnum,
|
||||
current_platform,
|
||||
)
|
||||
|
||||
VDN_MODEL_ID = "OpenVDN/vdn-minimax-h3"
|
||||
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA")
|
||||
# admission resolves hybrid_window_attn_h3, which only the CUDA platform registers
|
||||
requires_cuda_backend = pytest.mark.skipif(
|
||||
not current_platform.is_cuda(),
|
||||
reason="hybrid_window_attn_h3 admission needs NVIDIA CUDA",
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# registration and admission
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_registry_resolves_vdn_h3_configs() -> None:
|
||||
info = get_model_info(VDN_MODEL_ID)
|
||||
assert info.sampling_param_cls is VDNH3SamplingParams
|
||||
assert info.pipeline_config_cls is VDNH3PipelineConfig
|
||||
assert get_non_diffusers_pipeline_name(VDN_MODEL_ID) == "VDNH3Pipeline"
|
||||
assert get_non_diffusers_pipeline_name("/models/OpenVDN/vdn-minimax-h3") == (
|
||||
"VDNH3Pipeline"
|
||||
)
|
||||
# the base H3 detector must not swallow the VDN id (it contains "minimax-h3")
|
||||
base = get_model_info("MiniMaxAI/MiniMax-H3")
|
||||
assert base.pipeline_config_cls is MiniMaxH3PipelineConfig
|
||||
|
||||
|
||||
def test_vdn_h3_sampling_defaults_and_rejections() -> None:
|
||||
params = VDNH3SamplingParams(prompt="p")
|
||||
assert params.num_inference_steps == 9 # 8 NFE
|
||||
with pytest.raises(ValueError, match="exactly nine sigma grid points"):
|
||||
VDNH3SamplingParams(prompt="p", num_inference_steps=8)
|
||||
fl2va = VDNH3SamplingParams(
|
||||
prompt="p",
|
||||
task="fl2va",
|
||||
conditions=[
|
||||
{"type": "image", "uri": "x.png", "role": "keyframe", "frame_index": 0}
|
||||
],
|
||||
target={"short_edge": 768, "aspect_ratio": "auto", "duration_seconds": 5.0},
|
||||
)
|
||||
assert fl2va.task == "fl2va"
|
||||
with pytest.raises(ValueError, match="ref2va was not trained"):
|
||||
VDNH3SamplingParams(
|
||||
prompt="p",
|
||||
task="ref2va",
|
||||
conditions=[{"type": "image", "uri": "x.png", "role": "reference"}],
|
||||
target={"short_edge": 768, "aspect_ratio": "auto", "duration_seconds": 5.0},
|
||||
)
|
||||
|
||||
|
||||
def _server_args(**overrides) -> SimpleNamespace:
|
||||
args = dict(
|
||||
model_variant=None,
|
||||
attention_backend=None,
|
||||
component_attention_backends={},
|
||||
attention_backend_config=None,
|
||||
ring_degree=1,
|
||||
enable_torch_compile=False,
|
||||
enable_breakable_cuda_graph=False,
|
||||
quantization=None,
|
||||
)
|
||||
args.update(overrides)
|
||||
ns = SimpleNamespace(**args)
|
||||
ns.resolve_component_attention_backend = lambda name: (
|
||||
(
|
||||
AttentionBackendEnum[str(ns.component_attention_backends[name]).upper()]
|
||||
if name in ns.component_attention_backends
|
||||
else None
|
||||
),
|
||||
None,
|
||||
)
|
||||
return ns
|
||||
|
||||
|
||||
@requires_cuda_backend
|
||||
def test_vdn_h3_pipeline_config_rejections() -> None:
|
||||
config = VDNH3PipelineConfig()
|
||||
with pytest.raises(ValueError, match="--model-variant does not apply"):
|
||||
config.validate_server_args(_server_args(model_variant="ref2va"))
|
||||
with pytest.raises(
|
||||
ValueError, match="requires --attention-backend hybrid_window_attn_h3"
|
||||
):
|
||||
config.validate_server_args(_server_args(attention_backend="fa"))
|
||||
with pytest.raises(ValueError, match="ring-degree"):
|
||||
config.validate_server_args(_server_args(ring_degree=2))
|
||||
with pytest.raises(ValueError, match="torch.compile"):
|
||||
config.validate_server_args(_server_args(enable_torch_compile=True))
|
||||
with pytest.raises(ValueError, match="breakable CUDA graph"):
|
||||
config.validate_server_args(_server_args(enable_breakable_cuda_graph=True))
|
||||
with pytest.raises(ValueError, match="no.*audited high-quality deployment"):
|
||||
config.validate_quality_deployment(server_args=None)
|
||||
args = _server_args()
|
||||
config.validate_server_args(args)
|
||||
assert args.attention_backend == "hybrid_window_attn_h3"
|
||||
|
||||
|
||||
@requires_cuda_backend
|
||||
def test_vdn_h3_quantization_defaults_to_mxfp8_on_blackwell(monkeypatch) -> None:
|
||||
"""Online MXFP8 is the default on SM100+ (SM120 included) and what `fp8`
|
||||
maps to there; `bf16` opts out; before SM100 the block-scaled GEMM does
|
||||
not exist, so an unset flag stays bf16 and `fp8` stays the per-channel
|
||||
path."""
|
||||
from sglang.multimodal_gen.configs.pipeline_configs import minimax_h3_vdn as module
|
||||
|
||||
def resolved(
|
||||
quantization: str | None, blackwell: bool, sm120: bool = False
|
||||
) -> str | None:
|
||||
monkeypatch.setattr(module.current_platform, "is_blackwell", lambda: blackwell)
|
||||
monkeypatch.setattr(module.current_platform, "is_sm120", lambda: sm120)
|
||||
args = _server_args(quantization=quantization)
|
||||
VDNH3PipelineConfig().validate_server_args(args)
|
||||
return args.quantization
|
||||
|
||||
assert resolved(None, True) == "mxfp8"
|
||||
assert resolved("fp8", True) == "mxfp8"
|
||||
assert resolved("bf16", True) is None
|
||||
assert resolved(None, False) is None
|
||||
assert resolved("fp8", False) == "fp8"
|
||||
assert resolved(None, False, sm120=True) == "mxfp8"
|
||||
assert resolved("fp8", False, sm120=True) == "mxfp8"
|
||||
assert resolved("bf16", False, sm120=True) is None
|
||||
|
||||
|
||||
def test_hybrid_arch_config_from_transform_config_and_mapping() -> None:
|
||||
transform = {
|
||||
"anchor_frames": "both",
|
||||
"enable_softmax_gate": True,
|
||||
"linear_attention": {
|
||||
"a_fp32": True,
|
||||
"bridge": "alpha",
|
||||
"delta_rule": "vdn_solve",
|
||||
"enable_text_state": True,
|
||||
"linear_head_dim": 128,
|
||||
"short_conv": {"targets": ["k", "v"]},
|
||||
},
|
||||
"softmax_attention": {"chunk": 5, "radius": 1},
|
||||
}
|
||||
dit = MiniMaxH3DiTConfig()
|
||||
dit.update_model_arch({"hybrid_attention": transform, "num_layers": 2})
|
||||
hybrid = dit.arch_config.hybrid_attention
|
||||
assert isinstance(hybrid, VDNHybridAttentionArchConfig)
|
||||
assert hybrid.short_conv == ("k", "v") and hybrid.chunk == 5
|
||||
assert MiniMaxH3DiTConfig().arch_config.hybrid_attention is None
|
||||
with pytest.raises(ValueError, match="delta_rule"):
|
||||
VDNHybridAttentionArchConfig(delta_rule="bogus")
|
||||
|
||||
mapping = MiniMaxH3DiTArchConfig().param_names_mapping
|
||||
for source, expected in (
|
||||
(
|
||||
"transformer_blocks.7.attn.linear_attention.alpha.A_log",
|
||||
"blocks.7.attn.hybrid.linear_attention.alpha.A_log",
|
||||
),
|
||||
(
|
||||
"transformer_blocks.7.attn.softmax_gate.up.bias",
|
||||
"blocks.7.attn.hybrid.softmax_gate.up.bias",
|
||||
),
|
||||
(
|
||||
"transformer_blocks.7.attn.to_out_linear.weight",
|
||||
"blocks.7.attn.hybrid.to_out_linear.weight",
|
||||
),
|
||||
):
|
||||
targets = [
|
||||
re.sub(pattern, target if isinstance(target, str) else target[0], source)
|
||||
for pattern, target in mapping.items()
|
||||
if re.match(pattern, source)
|
||||
]
|
||||
assert targets == [expected], (source, targets)
|
||||
|
||||
|
||||
def test_layout_from_packed_t2va() -> None:
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.packed_sequence import (
|
||||
minimax_h3_packed_sequence,
|
||||
)
|
||||
|
||||
packed = minimax_h3_packed_sequence(
|
||||
text_len=70,
|
||||
latent_t=12,
|
||||
latent_h=12,
|
||||
latent_w=16,
|
||||
audio_t=50,
|
||||
include_keyframe_cond=False,
|
||||
)
|
||||
layout = vdn_h3_layout_from_packed(packed, latent_t=12, latent_h=12, latent_w=16)
|
||||
assert layout.text_len == 70
|
||||
assert layout.video_start == 70 + 100
|
||||
assert layout.tokens_per_frame == 48 and layout.frame_size == (6, 8)
|
||||
assert layout.used == 70 + 100 + 12 * 48
|
||||
assert layout.seq_len == int(packed["seq_len"]) and layout.seq_len % 64 == 0
|
||||
assert layout.global_ranges == [(0, 170)]
|
||||
|
||||
|
||||
def test_layout_from_packed_fl2va_keeps_keyframe_rows_global() -> None:
|
||||
"""Keyframe rows must land in the global ranges, not in the video span
|
||||
the linear branch scans."""
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.packed_sequence import (
|
||||
minimax_h3_packed_sequence,
|
||||
)
|
||||
|
||||
packed = minimax_h3_packed_sequence(
|
||||
text_len=70,
|
||||
latent_t=12,
|
||||
latent_h=12,
|
||||
latent_w=16,
|
||||
audio_t=50,
|
||||
include_keyframe_cond=True,
|
||||
keyframe_frame_indices=[0, -1],
|
||||
frame_count=45,
|
||||
)
|
||||
layout = vdn_h3_layout_from_packed(packed, latent_t=12, latent_h=12, latent_w=16)
|
||||
assert layout.text_len == 70
|
||||
assert layout.video_start == 70 + 2 * 48 + 100
|
||||
assert layout.used == layout.video_end == layout.video_start + 12 * 48
|
||||
assert layout.global_ranges == [(0, layout.video_start)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the linear branch arithmetic
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
FRAMES, HEADS, TOKENS, HEAD_DIM = 6, 2, 16, 32
|
||||
|
||||
|
||||
def _random_stats(device, seed=0):
|
||||
g = torch.Generator(device="cpu").manual_seed(seed)
|
||||
k = torch.nn.functional.normalize(
|
||||
torch.randn(FRAMES, HEADS, TOKENS, HEAD_DIM, generator=g), dim=-1
|
||||
).to(device)
|
||||
v = torch.randn(FRAMES, HEADS, TOKENS, HEAD_DIM, generator=g).to(device)
|
||||
beta = torch.rand(FRAMES, HEADS, TOKENS, generator=g).to(device)
|
||||
alpha = torch.rand(FRAMES, HEADS, HEAD_DIM, generator=g).to(device) * 0.5 + 0.5
|
||||
return k, v, beta, alpha
|
||||
|
||||
|
||||
def test_frame_statistics_and_delta_rule_match_dense_algebra() -> None:
|
||||
k, v, beta, alpha = _random_stats("cpu")
|
||||
A, B = frame_statistics(k, v, beta, a_fp32=True)
|
||||
A_ref = torch.einsum("fhsk,fhs,fhsl->fhkl", k, beta, k)
|
||||
B_ref = torch.einsum("fhsv,fhs,fhsk->fhvk", v, beta, k)
|
||||
assert torch.allclose(A, A_ref, atol=1e-4) and torch.allclose(B, B_ref, atol=1e-4)
|
||||
transition, injection = delta_factor_apply(
|
||||
"vdn_solve", alpha, A, B, tokens_per_frame=TOKENS
|
||||
)
|
||||
inv = torch.linalg.inv(torch.eye(HEAD_DIM) + A)
|
||||
assert torch.allclose(transition, alpha.unsqueeze(-1) * inv, atol=1e-4)
|
||||
assert torch.allclose(injection, B @ inv, atol=1e-4)
|
||||
|
||||
|
||||
def test_scans_match_step_reference_and_text_seed() -> None:
|
||||
k, v, beta, alpha = _random_stats("cpu", seed=1)
|
||||
A, B = frame_statistics(k, v, beta, a_fp32=True)
|
||||
transition, injection = delta_factor_apply(
|
||||
"vdn_solve", alpha, A, B, tokens_per_frame=TOKENS
|
||||
)
|
||||
text_state = torch.randn(HEADS, HEAD_DIM, HEAD_DIM)
|
||||
prefix, suffix = run_scans(transition, injection, text_state)
|
||||
state = text_state.clone()
|
||||
for f in range(FRAMES):
|
||||
state = state @ transition[f] + injection[f]
|
||||
assert torch.allclose(prefix[f], state, atol=1e-4)
|
||||
state = text_state.clone()
|
||||
for f in range(FRAMES - 1, -1, -1):
|
||||
state = state @ transition[f] + injection[f]
|
||||
assert torch.allclose(suffix[f], state, atol=1e-4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("world", [1, 2, 5, 10])
|
||||
def test_frame_partial_sums_match_index_add(world: int) -> None:
|
||||
"""The Ulysses frame-mean partial sums (reshape-sum over whole frames plus
|
||||
two edge rows sums, deterministic) equal the index_add formulation."""
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn_attention import (
|
||||
_vdn_frame_partial_sums,
|
||||
)
|
||||
|
||||
frames, tpf, hidden = 12, 50, 64
|
||||
video_start = 20
|
||||
video_end = video_start + frames * tpf
|
||||
seq = video_end + 30 # 650 rows, divisible by every ``world`` above
|
||||
g = torch.Generator(device="cpu").manual_seed(0)
|
||||
x = torch.randn(seq, hidden, generator=g).to(torch.bfloat16)
|
||||
local = seq // world
|
||||
total = torch.zeros(frames, hidden)
|
||||
for rank in range(world):
|
||||
total += _vdn_frame_partial_sums(
|
||||
x[rank * local : (rank + 1) * local],
|
||||
row_start=rank * local,
|
||||
video_start=video_start,
|
||||
video_end=video_end,
|
||||
num_frames=frames,
|
||||
tokens_per_frame=tpf,
|
||||
)
|
||||
ref = x[video_start:video_end].float().view(frames, tpf, hidden).sum(1)
|
||||
torch.testing.assert_close(total, ref, rtol=1e-5, atol=1e-3)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
||||
@pytest.mark.parametrize("anchor_frames", ["both", "none"])
|
||||
@pytest.mark.parametrize("reference", ["frame_chain_scans", "eager_kernels"])
|
||||
def test_branch_forward_matches_reference(
|
||||
anchor_frames: str, reference: str, monkeypatch
|
||||
) -> None:
|
||||
"""The shipped branch (boundary scans, fused Triton kernels) against the
|
||||
same module with the plain frame-chain scans, and against the eager
|
||||
kernel chain; both across the anchor-frame shift of the chunk grid."""
|
||||
from sglang.multimodal_gen.runtime.models.dits import minimax_h3_vdn as module
|
||||
|
||||
device = torch.device("cuda")
|
||||
hidden, heads, head_dim = 64, 4, 32
|
||||
num_frames, fh, fw = 13, 4, 6
|
||||
tpf = fh * fw
|
||||
hybrid = VDNHybridAttentionArchConfig(
|
||||
chunk=3, radius=1, anchor_frames=anchor_frames, linear_head_dim=head_dim
|
||||
)
|
||||
branch = _branch(hybrid, heads, hidden, head_dim, seed=0).to(device)
|
||||
layout = VDNH3Layout(
|
||||
seq_len=64 * 6,
|
||||
used=10 + num_frames * tpf,
|
||||
text_len=10,
|
||||
video_start=10,
|
||||
num_frames=num_frames,
|
||||
tokens_per_frame=tpf,
|
||||
frame_height=fh,
|
||||
frame_width=fw,
|
||||
)
|
||||
g = torch.Generator(device="cpu").manual_seed(1)
|
||||
V = num_frames * tpf
|
||||
q, k, v = (
|
||||
torch.randn(V, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(3)
|
||||
)
|
||||
tk, tv = (
|
||||
torch.randn(10, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(2)
|
||||
)
|
||||
x = torch.randn(V, hidden, generator=g).to(device, torch.bfloat16)
|
||||
tx = torch.randn(10, hidden, generator=g).to(device, torch.bfloat16)
|
||||
kwargs = dict(
|
||||
q_raw=q,
|
||||
k_raw=k,
|
||||
v_raw=v,
|
||||
beta=branch.beta(x),
|
||||
gate=branch.output_gate(x),
|
||||
frame_mean=x.view(num_frames, tpf, hidden).mean(1, dtype=torch.float32),
|
||||
layout=layout,
|
||||
text_k_raw=tk,
|
||||
text_v_raw=tv,
|
||||
text_beta=branch.beta(tx),
|
||||
)
|
||||
fast = branch(**kwargs)
|
||||
if reference == "frame_chain_scans":
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"run_boundary_scans",
|
||||
lambda t, i, ts, *, chunk, frame_offset=0: run_scans(t, i, ts),
|
||||
)
|
||||
else:
|
||||
branch.fused_kernels = False
|
||||
expected = branch(**kwargs)
|
||||
assert expected.abs().sum() > 0
|
||||
# fused kernels skip the eager chain's bf16 roundings; the scans re-associate fp32
|
||||
tolerance = {"frame_chain_scans": 5e-3, "eager_kernels": 2e-2}[reference]
|
||||
rel = (fast.float() - expected.float()).norm() / expected.float().norm()
|
||||
assert rel < tolerance, rel
|
||||
|
||||
|
||||
def test_gather_is_the_exact_window_complement() -> None:
|
||||
"""With alpha = 1 and one-hot frame indicators, the gathered state must
|
||||
be exactly the indicator of the frames outside the window."""
|
||||
num_frames = 9
|
||||
hybrid = VDNHybridAttentionArchConfig(chunk=3, radius=1, anchor_frames="none")
|
||||
bounds = hybrid.window_bounds(num_frames)
|
||||
# injection[f] = one-hot(f) laid along dv; transition = identity
|
||||
eye = torch.eye(num_frames)
|
||||
injection = (
|
||||
eye.view(num_frames, 1, num_frames, 1)
|
||||
.expand(num_frames, 1, num_frames, 1)
|
||||
.clone()
|
||||
)
|
||||
transition = torch.eye(1).view(1, 1, 1, 1).expand(num_frames, 1, 1, 1).clone()
|
||||
prefix, suffix = run_scans(transition, injection, None)
|
||||
alpha = torch.ones(num_frames, 1, 1)
|
||||
gathered = gather_linear_state(
|
||||
prefix,
|
||||
suffix,
|
||||
alpha,
|
||||
bounds,
|
||||
bridge="alpha",
|
||||
text_state=None,
|
||||
out_dtype=torch.float32,
|
||||
)
|
||||
for t in range(num_frames):
|
||||
lo, hi = max(bounds[t][0], 0), min(bounds[t][1], num_frames - 1)
|
||||
expected = torch.tensor(
|
||||
[1.0 if (f < lo or f > hi) else 0.0 for f in range(num_frames)]
|
||||
)
|
||||
assert torch.equal(gathered[t, 0, :, 0], expected), (t, gathered[t, 0, :, 0])
|
||||
|
||||
|
||||
def test_gather_text_state_decays_over_skipped_frames() -> None:
|
||||
"""A clip-end frame reads the text state decayed by prod alpha over
|
||||
exactly the frames between the boundary and t (VDN's bridge indices)."""
|
||||
num_frames = 4
|
||||
bounds = [
|
||||
(t, t) for t in range(num_frames)
|
||||
] # radius 0: complement = everything else
|
||||
prefix = torch.zeros(num_frames, 1, 1, 1)
|
||||
suffix = torch.zeros(num_frames, 1, 1, 1)
|
||||
alpha = torch.tensor([0.5, 0.25, 0.5, 0.5]).view(num_frames, 1, 1)
|
||||
text_state = torch.ones(1, 1, 1)
|
||||
out = gather_linear_state(
|
||||
prefix,
|
||||
suffix,
|
||||
alpha,
|
||||
bounds,
|
||||
bridge="alpha",
|
||||
text_state=text_state,
|
||||
out_dtype=torch.float32,
|
||||
).view(num_frames)
|
||||
# frame 0 reads the text state through alpha[0]; frame 3 through alpha[3]
|
||||
assert math.isclose(out[0].item(), 0.5, rel_tol=1e-6)
|
||||
assert math.isclose(out[3].item(), 0.5, rel_tol=1e-6)
|
||||
assert out[1].item() == 0.0, "both neighbours in range and zero"
|
||||
|
||||
|
||||
def test_temporal_shift_features_match_conv1d() -> None:
|
||||
from sglang.multimodal_gen.runtime.models.dits.minimax_h3_vdn import _temporal_shift
|
||||
|
||||
x = torch.randn(7, 5, 6) # [F, S, C]
|
||||
w = torch.randn(6, 5)
|
||||
got = _temporal_shift(x, w)
|
||||
ref = (
|
||||
torch.nn.functional.conv1d(
|
||||
x.permute(1, 2, 0).reshape(5, 6, 7), w.view(6, 1, 5), padding=2, groups=6
|
||||
)
|
||||
.reshape(5, 6, 7)
|
||||
.permute(2, 0, 1)
|
||||
)
|
||||
assert torch.allclose(got, ref, atol=1e-5)
|
||||
|
||||
|
||||
def _branch(
|
||||
hybrid: VDNHybridAttentionArchConfig,
|
||||
heads: int,
|
||||
hidden: int,
|
||||
head_dim: int,
|
||||
seed: int,
|
||||
):
|
||||
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
|
||||
maybe_init_distributed_environment_and_model_parallel,
|
||||
model_parallel_is_initialized,
|
||||
)
|
||||
from sglang.multimodal_gen.test.single_test_file.component_accuracy.utils import (
|
||||
ensure_distributed_env_defaults,
|
||||
)
|
||||
|
||||
if not model_parallel_is_initialized():
|
||||
ensure_distributed_env_defaults()
|
||||
maybe_init_distributed_environment_and_model_parallel(tp_size=1, sp_size=1)
|
||||
arch = MiniMaxH3DiTArchConfig(
|
||||
num_attention_heads=heads, attention_head_dim=head_dim, hidden_size=hidden
|
||||
)
|
||||
branch = MiniMaxH3VDNLinearBranch(arch, hybrid, local_heads=heads)
|
||||
g = torch.Generator(device="cpu").manual_seed(seed)
|
||||
with torch.no_grad():
|
||||
for name, p in branch.named_parameters():
|
||||
if name.endswith("A_log"):
|
||||
p.copy_(torch.log(torch.empty_like(p).uniform_(1, 16, generator=g)))
|
||||
elif name.endswith("dt_bias"):
|
||||
p.copy_(torch.randn(p.shape, generator=g) - 3)
|
||||
elif name.endswith("norm.weight"):
|
||||
p.copy_(torch.ones_like(p))
|
||||
else:
|
||||
p.copy_(
|
||||
torch.randn(p.shape, generator=g, dtype=torch.float32).to(p.dtype)
|
||||
* 0.1
|
||||
)
|
||||
return branch
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_branch_head_slice_equals_full_run() -> None:
|
||||
"""The Ulysses contract: the branch is per-head independent given
|
||||
(beta, gate, alpha), so a head-sliced run equals the full run's slice."""
|
||||
device = torch.device("cuda")
|
||||
hidden, heads, head_dim = 64, 4, 32
|
||||
num_frames, fh, fw = 7, 4, 6
|
||||
tpf = fh * fw
|
||||
hybrid = VDNHybridAttentionArchConfig(
|
||||
chunk=2, radius=1, anchor_frames="both", linear_head_dim=head_dim
|
||||
)
|
||||
branch = _branch(hybrid, heads, hidden, head_dim, seed=0).to(device)
|
||||
layout = VDNH3Layout(
|
||||
seq_len=64 * 4,
|
||||
used=10 + num_frames * tpf,
|
||||
text_len=10,
|
||||
video_start=10,
|
||||
num_frames=num_frames,
|
||||
tokens_per_frame=tpf,
|
||||
frame_height=fh,
|
||||
frame_width=fw,
|
||||
)
|
||||
g = torch.Generator(device="cpu").manual_seed(1)
|
||||
V = num_frames * tpf
|
||||
q, k, v = (
|
||||
torch.randn(V, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(3)
|
||||
)
|
||||
tk, tv = (
|
||||
torch.randn(10, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(2)
|
||||
)
|
||||
x = torch.randn(V, hidden, generator=g).to(device, torch.bfloat16)
|
||||
tx = torch.randn(10, hidden, generator=g).to(device, torch.bfloat16)
|
||||
beta, gate = branch.beta(x), branch.output_gate(x)
|
||||
tbeta = branch.beta(tx)
|
||||
frame_mean = x.view(num_frames, tpf, hidden).mean(1, dtype=torch.float32)
|
||||
full = branch(
|
||||
q_raw=q,
|
||||
k_raw=k,
|
||||
v_raw=v,
|
||||
beta=beta,
|
||||
gate=gate,
|
||||
frame_mean=frame_mean,
|
||||
layout=layout,
|
||||
text_k_raw=tk,
|
||||
text_v_raw=tv,
|
||||
text_beta=tbeta,
|
||||
).view(V, heads, head_dim)
|
||||
# anchors read zero
|
||||
assert torch.all(full[:tpf] == 0) and torch.all(full[-tpf:] == 0)
|
||||
assert full[tpf:-tpf].abs().sum() > 0
|
||||
|
||||
# the same module on a head range of the full sequence
|
||||
hs = slice(1, 3)
|
||||
part = branch(
|
||||
q_raw=q[:, hs],
|
||||
k_raw=k[:, hs],
|
||||
v_raw=v[:, hs],
|
||||
beta=beta[:, hs],
|
||||
gate=gate[:, hs],
|
||||
frame_mean=frame_mean,
|
||||
layout=layout,
|
||||
text_k_raw=tk[:, hs],
|
||||
text_v_raw=tv[:, hs],
|
||||
text_beta=tbeta[:, hs],
|
||||
heads=hs,
|
||||
).view(V, 2, head_dim)
|
||||
diff = (part.float() - full[:, hs].float()).abs().max().item()
|
||||
assert diff < 2e-2, f"head slice vs full run max diff {diff}"
|
||||
|
||||
|
||||
@requires_cuda
|
||||
def test_branch_matches_eager_reference_algorithm() -> None:
|
||||
"""The module against a from-scratch spelling of VDN's _readout (no
|
||||
skip_ends), including the text state seed."""
|
||||
device = torch.device("cuda")
|
||||
hidden, heads, head_dim = 48, 2, 32
|
||||
num_frames, fh, fw = 5, 3, 4
|
||||
tpf = fh * fw
|
||||
hybrid = VDNHybridAttentionArchConfig(
|
||||
chunk=0, radius=1, anchor_frames="none", linear_head_dim=head_dim, short_conv=()
|
||||
)
|
||||
branch = _branch(hybrid, heads, hidden, head_dim, seed=2).to(device)
|
||||
layout = VDNH3Layout(
|
||||
seq_len=256,
|
||||
used=8 + num_frames * tpf,
|
||||
text_len=8,
|
||||
video_start=8,
|
||||
num_frames=num_frames,
|
||||
tokens_per_frame=tpf,
|
||||
frame_height=fh,
|
||||
frame_width=fw,
|
||||
)
|
||||
g = torch.Generator(device="cpu").manual_seed(3)
|
||||
V = num_frames * tpf
|
||||
q, k, v = (
|
||||
torch.randn(V, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(3)
|
||||
)
|
||||
tk, tv = (
|
||||
torch.randn(8, heads, head_dim, generator=g).to(device, torch.bfloat16)
|
||||
for _ in range(2)
|
||||
)
|
||||
x = torch.randn(V, hidden, generator=g).to(device, torch.bfloat16)
|
||||
tx = torch.randn(8, hidden, generator=g).to(device, torch.bfloat16)
|
||||
beta, gate, tbeta = branch.beta(x), branch.output_gate(x), branch.beta(tx)
|
||||
frame_mean = x.view(num_frames, tpf, hidden).mean(1, dtype=torch.float32)
|
||||
got = branch(
|
||||
q_raw=q,
|
||||
k_raw=k,
|
||||
v_raw=v,
|
||||
beta=beta,
|
||||
gate=gate,
|
||||
frame_mean=frame_mean,
|
||||
layout=layout,
|
||||
text_k_raw=tk,
|
||||
text_v_raw=tv,
|
||||
text_beta=tbeta,
|
||||
)
|
||||
|
||||
# reference in fp32
|
||||
def feat(t, l2):
|
||||
y = torch.nn.functional.silu(t.float())
|
||||
return torch.nn.functional.normalize(y, dim=-1, eps=1e-6) if l2 else y
|
||||
|
||||
qf, kf, vf = feat(q, True), feat(k, True), feat(v, False)
|
||||
bounds = hybrid.window_bounds(num_frames)
|
||||
kb = kf.view(num_frames, tpf, heads, head_dim).permute(0, 2, 1, 3)
|
||||
vb = vf.view(num_frames, tpf, heads, head_dim).permute(0, 2, 1, 3)
|
||||
bb = beta.float().view(num_frames, tpf, heads).permute(0, 2, 1)
|
||||
A = torch.einsum("fhsk,fhs,fhsl->fhkl", kb, bb, kb)
|
||||
B = torch.einsum("fhsv,fhs,fhsk->fhvk", vb, bb, kb)
|
||||
inv = torch.linalg.inv(torch.eye(head_dim, device=device) + A)
|
||||
alpha = branch.alpha(frame_mean)
|
||||
trans = alpha.unsqueeze(-1) * inv
|
||||
inj = B @ inv
|
||||
# text state
|
||||
tkf, tvf = feat(tk, True), feat(tv, False)
|
||||
tkb = tkf.view(1, 8, heads, head_dim).permute(0, 2, 1, 3)
|
||||
tvb = tvf.view(1, 8, heads, head_dim).permute(0, 2, 1, 3)
|
||||
tbb = tbeta.float().view(1, 8, heads).permute(0, 2, 1)
|
||||
tA = torch.einsum("fhsk,fhs,fhsl->fhkl", tkb, tbb, tkb)[0]
|
||||
tB = torch.einsum("fhsv,fhs,fhsk->fhvk", tvb, tbb, tkb)[0]
|
||||
text_state = TEXT_STATE_SCALE * (
|
||||
tB @ torch.linalg.inv(torch.eye(head_dim, device=device) + tA)
|
||||
)
|
||||
prefix, suffix = [], [None] * num_frames
|
||||
s = text_state.clone()
|
||||
for f in range(num_frames):
|
||||
s = s @ trans[f] + inj[f]
|
||||
prefix.append(s)
|
||||
s = text_state.clone()
|
||||
for f in range(num_frames - 1, -1, -1):
|
||||
s = s @ trans[f] + inj[f]
|
||||
suffix[f] = s
|
||||
outs = []
|
||||
for t in range(num_frames):
|
||||
lo, hi = bounds[t]
|
||||
left = prefix[lo - 1] if lo - 1 >= 0 else text_state
|
||||
right = suffix[hi + 1] if hi + 1 < num_frames else text_state
|
||||
a_before = torch.prod(alpha[max(lo, 0) : t + 1], dim=0)
|
||||
a_after = torch.prod(alpha[t : min(hi, num_frames - 1) + 1], dim=0)
|
||||
state = left * a_before.unsqueeze(1) + right * a_after.unsqueeze(1)
|
||||
qt = qf.view(num_frames, tpf, heads, head_dim)[t] # [S, H, d]
|
||||
ro = torch.einsum("shk,hvk->shv", qt, state)
|
||||
ms = ro.pow(2).mean(-1, keepdim=True)
|
||||
ro = ro * torch.rsqrt(ms + branch.norm.eps) * branch.norm.weight.float()
|
||||
outs.append(ro)
|
||||
ref = (torch.cat(outs) * gate.float()).reshape(V, heads * head_dim)
|
||||
diff = (got.float() - ref).abs().max().item()
|
||||
scale = ref.abs().max().item()
|
||||
assert diff < 3e-2 * max(scale, 1.0), (
|
||||
f"branch vs reference max diff {diff} (scale {scale})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,115 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Online MXFP8 GEMM path (``MXFP8Config()`` on a bf16 checkpoint): load-time
|
||||
block quant, the prequantized (e4m3, swizzled scales) input from the fused
|
||||
SwiGLU kernel, and the per-layer fallback to the per-channel fp8 path."""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
||||
|
||||
|
||||
def _init_parallel() -> None:
|
||||
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
|
||||
maybe_init_distributed_environment_and_model_parallel,
|
||||
model_parallel_is_initialized,
|
||||
)
|
||||
from sglang.multimodal_gen.test.single_test_file.component_accuracy.utils import (
|
||||
ensure_distributed_env_defaults,
|
||||
)
|
||||
|
||||
if not model_parallel_is_initialized():
|
||||
ensure_distributed_env_defaults()
|
||||
maybe_init_distributed_environment_and_model_parallel(tp_size=1, sp_size=1)
|
||||
|
||||
|
||||
def _layer(
|
||||
in_f: int, out_f: int, bias: bool, params_dtype: torch.dtype = torch.bfloat16
|
||||
):
|
||||
from sglang.multimodal_gen.runtime.layers.linear import RowParallelLinear
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.mxfp8 import MXFP8Config
|
||||
|
||||
return RowParallelLinear(
|
||||
in_f,
|
||||
out_f,
|
||||
bias=bias,
|
||||
params_dtype=params_dtype,
|
||||
quant_config=MXFP8Config(),
|
||||
prefix="mlp.fc2",
|
||||
).to("cuda")
|
||||
|
||||
|
||||
def test_mxfp8_linear_matches_bf16_and_accepts_prequantized() -> None:
|
||||
if torch.cuda.get_device_capability()[0] < 10:
|
||||
pytest.skip("cuBLASLt MXFP8 block scaling requires Blackwell or newer")
|
||||
_init_parallel()
|
||||
from sglang.kernels.ops.diffusion import silu_mul_mxfp8
|
||||
|
||||
in_f, out_f, rows = 512, 384, 200
|
||||
layer = _layer(in_f, out_f, bias=False)
|
||||
g = torch.Generator(device="cpu").manual_seed(1)
|
||||
weight = (torch.randn(out_f, in_f, generator=g) * 0.02).to("cuda", torch.bfloat16)
|
||||
with torch.no_grad():
|
||||
layer.weight.copy_(weight)
|
||||
layer.quant_method.process_weights_after_loading(layer)
|
||||
assert layer.mxfp8 and layer.quant_method.accepts_mxfp8_input(layer)
|
||||
assert layer.weight.dtype == torch.float8_e4m3fn
|
||||
assert layer.weight_scale.dtype == torch.float8_e8m0fnu
|
||||
|
||||
x = torch.randn(rows, in_f, generator=g).to("cuda", torch.bfloat16)
|
||||
out, _ = layer(x)
|
||||
ref = x.float() @ weight.float().t()
|
||||
rel = ((out.float() - ref).norm() / ref.norm()).item()
|
||||
assert rel < 0.05, rel
|
||||
|
||||
hidden = torch.randn(rows, 2 * in_f, generator=g).to("cuda", torch.bfloat16)
|
||||
act = torch.nn.functional.silu(hidden[:, :in_f]) * hidden[:, in_f:]
|
||||
out_tensor, _ = layer(act)
|
||||
out_tuple, _ = layer(silu_mul_mxfp8(hidden))
|
||||
assert torch.equal(out_tensor, out_tuple)
|
||||
|
||||
|
||||
def _assert_aligned_layer_falls_back(params_dtype: torch.dtype) -> None:
|
||||
_init_parallel()
|
||||
layer = _layer(512, 384, bias=False, params_dtype=params_dtype)
|
||||
g = torch.Generator(device="cpu").manual_seed(1)
|
||||
weight = (torch.randn(384, 512, generator=g) * 0.02).to("cuda", params_dtype)
|
||||
with torch.no_grad():
|
||||
layer.weight.copy_(weight)
|
||||
layer.quant_method.process_weights_after_loading(layer)
|
||||
assert not layer.mxfp8 and not layer.quant_method.accepts_mxfp8_input(layer)
|
||||
x = torch.randn(200, 512, generator=g).to("cuda", params_dtype)
|
||||
out, _ = layer(x)
|
||||
ref = x.float() @ weight.float().t()
|
||||
rel = ((out.float() - ref).norm() / ref.norm()).item()
|
||||
assert rel < 0.05, rel
|
||||
|
||||
|
||||
def test_pre_blackwell_aligned_layer_falls_back_to_channelwise() -> None:
|
||||
if torch.cuda.get_device_capability()[0] >= 10:
|
||||
pytest.skip("requires a pre-Blackwell GPU")
|
||||
_assert_aligned_layer_falls_back(torch.bfloat16)
|
||||
|
||||
|
||||
def test_fp16_layer_falls_back_to_channelwise() -> None:
|
||||
"""The swizzled quantizer takes bf16 only; an fp16 layer must not fail at load."""
|
||||
_assert_aligned_layer_falls_back(torch.float16)
|
||||
|
||||
|
||||
def test_unaligned_layer_falls_back_to_channelwise() -> None:
|
||||
"""The block-scaled GEMM needs K % 32 == 0; such a layer keeps the
|
||||
per-channel fp8 path and still answers a forward. K is 16 rather than a
|
||||
smaller odd size because the fallback's scaled GEMM still wants K % 16 == 0
|
||||
(ROCm rejects anything else outright)."""
|
||||
_init_parallel()
|
||||
layer = _layer(16, 128, bias=True)
|
||||
layer.quant_method.process_weights_after_loading(layer)
|
||||
assert not layer.mxfp8 and not layer.quant_method.accepts_mxfp8_input(layer)
|
||||
out, _ = layer(torch.randn(4, 16, device="cuda", dtype=torch.bfloat16))
|
||||
assert out.shape == (4, 128)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
Reference in New Issue
Block a user