Add Vortex NVFP4 scale v1

This commit is contained in:
Daniel Maddern 2026-08-15 02:11:47 +07:00
parent 684b645546
commit 1cc0ffdd00
3 changed files with 157 additions and 1 deletions

View file

@ -1,6 +1,7 @@
#include <torch/extension.h>
torch::Tensor nvfp4_activation_scale_cuda(torch::Tensor input, double divisor);
torch::Tensor nvfp4_activation_scale_into_cuda(torch::Tensor input, double divisor, torch::Tensor partials, torch::Tensor output, int64_t blocks, int64_t threads);
torch::Tensor nvfp4_activation_scale(torch::Tensor input, double divisor) {
TORCH_CHECK(input.is_cuda(), "nvfp4_activation_scale expects a CUDA tensor");
@ -9,6 +10,23 @@ torch::Tensor nvfp4_activation_scale(torch::Tensor input, double divisor) {
return nvfp4_activation_scale_cuda(input, divisor);
}
torch::Tensor nvfp4_activation_scale_into(torch::Tensor input, double divisor, torch::Tensor partials, torch::Tensor output, int64_t blocks, int64_t threads) {
TORCH_CHECK(input.is_cuda(), "nvfp4_activation_scale_into expects a CUDA tensor");
TORCH_CHECK(input.is_contiguous(), "nvfp4_activation_scale_into expects contiguous input");
TORCH_CHECK(input.dim() == 2, "nvfp4_activation_scale_into expects a 2D tensor");
TORCH_CHECK(partials.is_cuda() && output.is_cuda(), "nvfp4_activation_scale_into workspace must be CUDA tensors");
TORCH_CHECK(partials.device() == input.device() && output.device() == input.device(), "nvfp4_activation_scale_into workspace must be on the input device");
TORCH_CHECK(partials.is_contiguous() && output.is_contiguous(), "nvfp4_activation_scale_into workspace must be contiguous");
TORCH_CHECK(partials.scalar_type() == torch::kFloat32 && output.scalar_type() == torch::kFloat32, "nvfp4_activation_scale_into workspace must be float32");
TORCH_CHECK(blocks > 0, "nvfp4_activation_scale_into blocks must be positive");
TORCH_CHECK(threads > 0 && threads <= 1024, "nvfp4_activation_scale_into threads must be between 1 and 1024");
TORCH_CHECK((threads & (threads - 1)) == 0, "nvfp4_activation_scale_into threads must be a power of two");
TORCH_CHECK(partials.numel() >= blocks, "nvfp4_activation_scale_into partial workspace is too small");
TORCH_CHECK(output.numel() >= 1, "nvfp4_activation_scale_into output workspace is too small");
return nvfp4_activation_scale_into_cuda(input, divisor, partials, output, blocks, threads);
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("nvfp4_activation_scale", &nvfp4_activation_scale, "Vortex NVFP4 activation scale");
m.def("nvfp4_activation_scale_into", &nvfp4_activation_scale_into, "Vortex NVFP4 activation scale with caller workspace");
}

View file

@ -3,12 +3,46 @@
#include <torch/extension.h>
#include <cmath>
#include <cstdint>
#include <limits>
namespace {
constexpr int kThreads = 256;
__inline__ __device__ float warp_reduce_max(float value) {
for (int offset = 16; offset > 0; offset >>= 1) {
value = fmaxf(value, __shfl_down_sync(0xffffffff, value, offset));
}
return value;
}
__inline__ __device__ float block_reduce_max(float value) {
__shared__ float warp_values[32];
const int lane = threadIdx.x & 31;
const int warp = threadIdx.x >> 5;
value = warp_reduce_max(value);
if (lane == 0) {
warp_values[warp] = value;
}
__syncthreads();
value = threadIdx.x < ((blockDim.x + 31) >> 5) ? warp_values[lane] : 0.0f;
if (warp == 0) {
value = warp_reduce_max(value);
}
return value;
}
__inline__ __device__ uint32_t max_bf16_abs_bits(uint32_t current, uint32_t packed) {
const uint32_t lo = packed & 0x7fffu;
const uint32_t hi = (packed >> 16) & 0x7fffu;
return max(current, max(lo, hi));
}
__inline__ __device__ float bf16_abs_bits_to_float(uint32_t bits) {
return __uint_as_float(bits << 16);
}
template <typename scalar_t>
__global__ void partial_absmax_kernel(const scalar_t* __restrict__ input, float* __restrict__ partials, int64_t numel) {
__shared__ float shared[kThreads];
@ -54,6 +88,40 @@ __global__ void final_scale_kernel(const float* __restrict__ partials, float* __
}
}
__global__ void partial_absmax_bf16_vec_kernel(const uint4* __restrict__ input, const uint16_t* __restrict__ scalar_input, float* __restrict__ partials, int64_t vector_count, int64_t numel) {
const int64_t stride = static_cast<int64_t>(blockDim.x) * gridDim.x;
int64_t index = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
uint32_t local_bits = 0;
while (index < vector_count) {
const uint4 values = input[index];
local_bits = max_bf16_abs_bits(local_bits, values.x);
local_bits = max_bf16_abs_bits(local_bits, values.y);
local_bits = max_bf16_abs_bits(local_bits, values.z);
local_bits = max_bf16_abs_bits(local_bits, values.w);
index += stride;
}
const int64_t tail_start = vector_count * 8;
for (int64_t tail = tail_start + static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x; tail < numel; tail += stride) {
local_bits = max(local_bits, static_cast<uint32_t>(scalar_input[tail] & 0x7fffu));
}
const float local_max = bf16_abs_bits_to_float(local_bits);
const float block_max = block_reduce_max(local_max);
if (threadIdx.x == 0) {
partials[blockIdx.x] = block_max;
}
}
__global__ void final_scale_warp_kernel(const float* __restrict__ partials, float* __restrict__ output, int64_t count, float divisor) {
float local_max = 0.0f;
for (int64_t index = threadIdx.x; index < count; index += blockDim.x) {
local_max = fmaxf(local_max, partials[index]);
}
const float block_max = block_reduce_max(local_max);
if (threadIdx.x == 0) {
output[0] = block_max / divisor;
}
}
} // namespace
torch::Tensor nvfp4_activation_scale_cuda(torch::Tensor input, double divisor) {
@ -76,3 +144,30 @@ torch::Tensor nvfp4_activation_scale_cuda(torch::Tensor input, double divisor) {
C10_CUDA_KERNEL_LAUNCH_CHECK();
return output;
}
torch::Tensor nvfp4_activation_scale_into_cuda(torch::Tensor input, double divisor, torch::Tensor partials, torch::Tensor output, int64_t blocks, int64_t threads) {
c10::cuda::CUDAGuard device_guard(input.device());
const auto numel = input.numel();
TORCH_CHECK(numel > 0, "nvfp4_activation_scale_into input must be non-empty");
TORCH_CHECK(divisor > 0.0, "nvfp4_activation_scale_into divisor must be positive");
TORCH_CHECK(input.scalar_type() == at::ScalarType::BFloat16, "nvfp4_activation_scale_into v1 currently expects bfloat16 input");
TORCH_CHECK(threads == 128 || threads == 256 || threads == 512, "nvfp4_activation_scale_into threads must be 128, 256, or 512");
TORCH_CHECK(blocks > 0, "nvfp4_activation_scale_into blocks must be positive");
TORCH_CHECK(reinterpret_cast<uintptr_t>(input.data_ptr()) % alignof(uint4) == 0, "nvfp4_activation_scale_into expects 16-byte aligned input");
const int launch_blocks = static_cast<int>(blocks);
const int launch_threads = static_cast<int>(threads);
const int64_t vector_count = numel / 8;
auto stream = at::cuda::getCurrentCUDAStream();
partial_absmax_bf16_vec_kernel<<<launch_blocks, launch_threads, 0, stream>>>(
reinterpret_cast<const uint4*>(input.data_ptr()),
reinterpret_cast<const uint16_t*>(input.data_ptr()),
partials.data_ptr<float>(),
vector_count,
numel);
C10_CUDA_KERNEL_LAUNCH_CHECK();
final_scale_warp_kernel<<<1, launch_threads, 0, stream>>>(partials.data_ptr<float>(), output.data_ptr<float>(), blocks, static_cast<float>(divisor));
C10_CUDA_KERNEL_LAUNCH_CHECK();
return output;
}

View file

@ -10,6 +10,9 @@ from pathlib import Path
import torch
_VORTEX_SCALE_WORKSPACES: dict[tuple[int, int], tuple[torch.Tensor, torch.Tensor]] = {}
def _sync() -> None:
if torch.cuda.is_available():
torch.cuda.synchronize()
@ -40,6 +43,36 @@ def _vortex_scale_extension():
)
def _env_int(name: str, default: int) -> int:
value = os.getenv(name)
if value is None or value == "":
return default
return int(value)
def _vortex_scale_geometry(numel: int) -> tuple[int, int]:
threads = _env_int("H3_NVFP4_SCALE_THREADS", 256)
if threads not in {128, 256, 512}:
raise ValueError(f"H3_NVFP4_SCALE_THREADS must be 128, 256, or 512, got {threads}")
default_blocks = min((numel + threads - 1) // threads, 4096)
blocks = _env_int("H3_NVFP4_SCALE_BLOCKS", default_blocks)
if blocks <= 0:
raise ValueError(f"H3_NVFP4_SCALE_BLOCKS must be positive, got {blocks}")
return blocks, threads
def _vortex_scale_workspace(tensor: torch.Tensor, blocks: int) -> tuple[torch.Tensor, torch.Tensor]:
key = (tensor.device.index or 0, blocks)
workspace = _VORTEX_SCALE_WORKSPACES.get(key)
if workspace is None or workspace[0].device != tensor.device:
workspace = (
torch.empty((blocks,), device=tensor.device, dtype=torch.float32),
torch.empty((), device=tensor.device, dtype=torch.float32),
)
_VORTEX_SCALE_WORKSPACES[key] = workspace
return workspace
def nvfp4_activation_scale(tensor: torch.Tensor, *, timings: dict[str, list[float]] | None = None) -> torch.Tensor:
"""Compute Comfy Kitchen's current per-tensor NVFP4 activation scale."""
from comfy_kitchen.float_utils import F4_E2M1_MAX, F8_E4M3_MAX
@ -55,7 +88,17 @@ def nvfp4_activation_scale(tensor: torch.Tensor, *, timings: dict[str, list[floa
raise ValueError("Vortex NVFP4 scale backend requires contiguous input")
try:
extension = _vortex_scale_extension()
scale = _record_timing(timings, "vortex_absmax_scale", lambda: extension.nvfp4_activation_scale(tensor, divisor))
version = os.getenv("H3_NVFP4_SCALE_VERSION", "1").lower()
if version in {"1", "v1"} and tensor.dtype == torch.bfloat16 and tensor.data_ptr() % 16 == 0:
blocks, threads = _vortex_scale_geometry(tensor.numel())
partials, output = _vortex_scale_workspace(tensor, blocks)
scale = _record_timing(
timings,
"vortex_absmax_scale",
lambda: extension.nvfp4_activation_scale_into(tensor, divisor, partials, output, blocks, threads),
)
else:
scale = _record_timing(timings, "vortex_absmax_scale", lambda: extension.nvfp4_activation_scale(tensor, divisor))
return _record_timing(timings, "scale_compat_cast", lambda: scale.to(tensor.dtype) if tensor.dtype != torch.float32 else scale)
except Exception:
if os.getenv("H3_NVFP4_SCALE_STRICT", "").lower() in {"1", "true", "yes", "on"}: