Mage-Flow-NVFP4-Balanced-AJH / runtime /packed_nvfp4_linear.py
ajh-code's picture
Add files using upload-large-folder tool
ca8b3a9 verified
Raw
History Blame Contribute Delete
17.5 kB
"""Inference-only resident NVFP4 linear prototype.
This module intentionally uses a narrow ctypes boundary. It proves packed
residency and Mage shape correctness; it is not yet a torch.compile/CUDA-graph
shipping operator.
"""
from __future__ import annotations
import atexit
import ctypes
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Final
import torch
from torch import nn
ABI_VERSION: Final = 1
FP4_BLOCK_ELEMENTS: Final = 16
SCALE_TILE_OUTER: Final = 128
SCALE_TILE_INNER: Final = 4
RELEASE_ROOT: Final = Path(__file__).resolve().parents[1]
DEFAULT_LIBRARY_PATH: Final = RELEASE_ROOT / "runtime" / "libmage_nvfp4_linear.so"
def round_up(value: int, multiple: int) -> int:
if value <= 0 or multiple <= 0:
raise ValueError("value and multiple must be positive")
return ((value + multiple - 1) // multiple) * multiple
@dataclass(frozen=True)
class ScaleLayout:
inner_dim: int
outer_tiles: int
num_bytes: int
def scale_layout(k: int, outer_columns: int) -> ScaleLayout:
if k <= 0 or k % FP4_BLOCK_ELEMENTS:
raise ValueError("K must be positive and divisible by 16")
if outer_columns <= 0:
raise ValueError("outer column count must be positive")
inner_dim = round_up(k // FP4_BLOCK_ELEMENTS, SCALE_TILE_INNER)
outer_tiles = (outer_columns + SCALE_TILE_OUTER - 1) // SCALE_TILE_OUTER
return ScaleLayout(
inner_dim=inner_dim,
outer_tiles=outer_tiles,
num_bytes=outer_tiles * inner_dim * SCALE_TILE_OUTER,
)
def packed_weight_num_bytes(out_features: int, in_features: int) -> int:
if out_features <= 0 or out_features % 8:
raise ValueError("out_features must be positive and divisible by 8")
if in_features <= 0 or in_features % 32:
raise ValueError("in_features must be positive and divisible by 32")
return out_features * in_features // 2
def padded_output_shape(input_shape: tuple[int, ...], out_features: int) -> tuple[int, int]:
if not input_shape:
raise ValueError("input must have at least one dimension")
logical_m = 1
for dimension in input_shape[:-1]:
if dimension <= 0:
raise ValueError("empty or negative leading dimensions are unsupported")
logical_m *= dimension
return round_up(logical_m, 8), out_features
def logical_output_shape(input_shape: tuple[int, ...], out_features: int) -> tuple[int, ...]:
if not input_shape:
raise ValueError("input must have at least one dimension")
return (*input_shape[:-1], out_features)
class NativeNvfp4Library:
"""Typed ctypes access to the project-local native library."""
def __init__(self, path: str | Path = DEFAULT_LIBRARY_PATH):
self.path = Path(path).resolve()
if not self.path.is_file():
raise FileNotFoundError(
f"resident NVFP4 library is not built: {self.path}"
)
self._library = ctypes.CDLL(str(self.path))
self._bind()
version = int(self._library.mage_nvfp4_abi_version())
if version != ABI_VERSION:
raise RuntimeError(
f"resident NVFP4 ABI mismatch: Python={ABI_VERSION}, native={version}"
)
def _bind(self) -> None:
library = self._library
library.mage_nvfp4_abi_version.argtypes = []
library.mage_nvfp4_abi_version.restype = ctypes.c_int
library.mage_nvfp4_last_error.argtypes = []
library.mage_nvfp4_last_error.restype = ctypes.c_char_p
library.mage_nvfp4_packed_weight_bytes.argtypes = [
ctypes.c_int,
ctypes.c_int,
]
library.mage_nvfp4_packed_weight_bytes.restype = ctypes.c_size_t
library.mage_nvfp4_weight_scale_bytes.argtypes = [
ctypes.c_int,
ctypes.c_int,
]
library.mage_nvfp4_weight_scale_bytes.restype = ctypes.c_size_t
library.mage_nvfp4_pack_weight_bf16.argtypes = [
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.POINTER(ctypes.c_float),
]
library.mage_nvfp4_pack_weight_bf16.restype = ctypes.c_int
library.mage_nvfp4_create_context.argtypes = [
ctypes.c_int,
ctypes.POINTER(ctypes.c_void_p),
]
library.mage_nvfp4_create_context.restype = ctypes.c_int
library.mage_nvfp4_destroy_context.argtypes = [ctypes.c_void_p]
library.mage_nvfp4_destroy_context.restype = ctypes.c_int
library.mage_nvfp4_context_reserved_bytes.argtypes = [ctypes.c_void_p]
library.mage_nvfp4_context_reserved_bytes.restype = ctypes.c_size_t
library.mage_nvfp4_linear_forward.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_size_t,
]
library.mage_nvfp4_linear_forward.restype = ctypes.c_int
def _error(self, operation: str) -> RuntimeError:
raw = self._library.mage_nvfp4_last_error()
message = raw.decode("utf-8", errors="replace") if raw else "unknown error"
return RuntimeError(f"{operation}: {message}")
def pack_weight(
self, weight: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if weight.device.type != "cpu":
raise ValueError("native weight packer requires a CPU tensor")
if weight.dtype != torch.bfloat16:
raise TypeError("native weight packer requires BF16")
if weight.ndim != 2 or not weight.is_contiguous():
raise ValueError("weight must be contiguous [N,K]")
n, k = map(int, weight.shape)
packed_bytes = packed_weight_num_bytes(n, k)
scale_bytes = scale_layout(k, n).num_bytes
native_packed = int(
self._library.mage_nvfp4_packed_weight_bytes(n, k)
)
native_scales = int(
self._library.mage_nvfp4_weight_scale_bytes(n, k)
)
if (native_packed, native_scales) != (packed_bytes, scale_bytes):
raise RuntimeError(
"Python/native packed metadata disagreement: "
f"Python={(packed_bytes, scale_bytes)}, "
f"native={(native_packed, native_scales)}"
)
packed = torch.empty(packed_bytes, dtype=torch.uint8, device="cpu")
scales = torch.empty(scale_bytes, dtype=torch.uint8, device="cpu")
tensor_scale = ctypes.c_float()
status = self._library.mage_nvfp4_pack_weight_bf16(
ctypes.c_void_p(weight.data_ptr()),
n,
k,
ctypes.c_void_p(packed.data_ptr()),
packed.numel(),
ctypes.c_void_p(scales.data_ptr()),
scales.numel(),
ctypes.byref(tensor_scale),
)
if status:
raise self._error("packing BF16 weight")
scale_tensor = torch.tensor(tensor_scale.value, dtype=torch.float32)
return packed, scales, scale_tensor
_LIBRARY_LOCK = threading.Lock()
_LIBRARY: NativeNvfp4Library | None = None
def native_library() -> NativeNvfp4Library:
global _LIBRARY
with _LIBRARY_LOCK:
if _LIBRARY is None:
_LIBRARY = NativeNvfp4Library()
return _LIBRARY
class ResidentContext:
def __init__(self, library: NativeNvfp4Library, device_index: int, stream: int):
self.library = library
self.device_index = int(device_index)
self.stream = int(stream)
self._pointer = ctypes.c_void_p()
self._lock = threading.Lock()
status = self.library._library.mage_nvfp4_create_context(
self.device_index, ctypes.byref(self._pointer)
)
if status:
raise self.library._error("creating resident context")
self._closed = False
@property
def pointer(self) -> ctypes.c_void_p:
if self._closed:
raise RuntimeError("resident NVFP4 context is closed")
return self._pointer
@property
def reserved_bytes(self) -> int:
return int(
self.library._library.mage_nvfp4_context_reserved_bytes(self.pointer)
)
def close(self) -> None:
with self._lock:
if self._closed:
return
status = self.library._library.mage_nvfp4_destroy_context(
self._pointer
)
if status:
raise self.library._error("destroying resident context")
self._closed = True
self._pointer = ctypes.c_void_p()
def forward(
self,
x: torch.Tensor,
packed_weight: torch.Tensor,
weight_scales: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
output: torch.Tensor,
logical_m: int,
in_features: int,
out_features: int,
) -> None:
bias_pointer = (
ctypes.c_void_p(bias.data_ptr()) if bias is not None else None
)
with self._lock:
status = self.library._library.mage_nvfp4_linear_forward(
self.pointer,
ctypes.c_void_p(x.data_ptr()),
ctypes.c_void_p(packed_weight.data_ptr()),
packed_weight.numel(),
ctypes.c_void_p(weight_scales.data_ptr()),
weight_scales.numel(),
ctypes.c_void_p(weight_scale.data_ptr()),
bias_pointer,
ctypes.c_void_p(output.data_ptr()),
logical_m,
in_features,
out_features,
self.stream,
)
if status:
raise self.library._error("resident NVFP4 linear forward")
_CONTEXTS_LOCK = threading.Lock()
_CONTEXTS: dict[tuple[int, int], ResidentContext] = {}
def resident_context(device: torch.device) -> ResidentContext:
if device.type != "cuda":
raise ValueError("resident NVFP4 context requires CUDA")
device_index = (
torch.cuda.current_device() if device.index is None else int(device.index)
)
stream = int(torch.cuda.current_stream(device_index).cuda_stream)
key = (device_index, stream)
with _CONTEXTS_LOCK:
context = _CONTEXTS.get(key)
if context is None:
context = ResidentContext(native_library(), device_index, stream)
_CONTEXTS[key] = context
return context
def close_all_contexts() -> None:
with _CONTEXTS_LOCK:
contexts = list(_CONTEXTS.values())
_CONTEXTS.clear()
errors: list[Exception] = []
for context in contexts:
try:
context.close()
except Exception as error: # pragma: no cover - shutdown diagnostic
errors.append(error)
if errors:
raise RuntimeError(
"one or more resident NVFP4 contexts failed to close: "
+ "; ".join(map(str, errors))
)
def _quiet_atexit_close() -> None:
try:
close_all_contexts()
except Exception:
# CUDA may already be shutting down. Explicit close_all_contexts() is
# the auditable path; atexit is only a best-effort fallback.
pass
atexit.register(_quiet_atexit_close)
class PackedNvfp4Linear(nn.Module):
"""Packed inference replacement for one BF16 ``nn.Linear``."""
def __init__(
self,
in_features: int,
out_features: int,
packed_weight: torch.Tensor,
weight_scales: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
):
super().__init__()
expected_weight = packed_weight_num_bytes(out_features, in_features)
expected_scales = scale_layout(in_features, out_features).num_bytes
if (
packed_weight.dtype != torch.uint8
or packed_weight.ndim != 1
or not packed_weight.is_contiguous()
or packed_weight.numel() != expected_weight
):
raise ValueError("packed_weight has invalid dtype, shape, or size")
if (
weight_scales.dtype != torch.uint8
or weight_scales.ndim != 1
or not weight_scales.is_contiguous()
or weight_scales.numel() != expected_scales
):
raise ValueError("weight_scales has invalid dtype, shape, or size")
if (
weight_scale.dtype != torch.float32
or weight_scale.numel() != 1
or not weight_scale.is_contiguous()
):
raise ValueError("weight_scale must be one contiguous FP32 value")
if bias is not None and (
bias.dtype != torch.bfloat16
or bias.shape != (out_features,)
or not bias.is_contiguous()
):
raise ValueError("bias must be contiguous BF16 [out_features]")
devices = {
tensor.device
for tensor in (packed_weight, weight_scales, weight_scale, bias)
if tensor is not None
}
if len(devices) != 1:
raise ValueError("all resident buffers must be on one device")
self.in_features = int(in_features)
self.out_features = int(out_features)
self.register_buffer("packed_weight", packed_weight)
self.register_buffer("weight_scales", weight_scales)
self.register_buffer("weight_scale", weight_scale.reshape(()))
self.register_buffer("bias", bias)
@classmethod
def from_linear(
cls,
linear: nn.Linear,
device: torch.device | str,
*,
library: NativeNvfp4Library | None = None,
) -> "PackedNvfp4Linear":
if not isinstance(linear, nn.Linear):
raise TypeError("from_linear requires torch.nn.Linear")
library = native_library() if library is None else library
destination = torch.device(device)
if destination.type != "cuda":
raise ValueError("resident packed buffers must target CUDA")
weight_cpu = (
linear.weight.detach()
.to(device="cpu", dtype=torch.bfloat16)
.contiguous()
)
packed, scales, tensor_scale = library.pack_weight(weight_cpu)
bias_cpu = (
None
if linear.bias is None
else linear.bias.detach()
.to(device="cpu", dtype=torch.bfloat16)
.contiguous()
)
return cls(
linear.in_features,
linear.out_features,
packed.to(destination),
scales.to(destination),
tensor_scale.to(destination),
None if bias_cpu is None else bias_cpu.to(destination),
)
def forward(self, input: torch.Tensor) -> torch.Tensor:
if input.device.type != "cuda":
raise ValueError("PackedNvfp4Linear requires a CUDA input")
if input.device != self.packed_weight.device:
raise ValueError("input and packed buffers are on different devices")
if input.dtype != torch.bfloat16:
raise TypeError("PackedNvfp4Linear requires BF16 input")
if input.ndim < 1 or input.shape[-1] != self.in_features:
raise ValueError(
f"expected last dimension {self.in_features}, got "
f"{tuple(input.shape)}"
)
if input.requires_grad:
raise RuntimeError("resident NVFP4 prototype is inference-only")
contiguous = input.reshape(-1, self.in_features).contiguous()
logical_m = int(contiguous.shape[0])
if logical_m <= 0:
raise ValueError("empty inputs are unsupported")
padded_m = round_up(logical_m, 8)
padded_output = torch.empty(
(padded_m, self.out_features),
dtype=torch.bfloat16,
device=input.device,
)
current_stream = torch.cuda.current_stream(input.device)
# ctypes launches are invisible to the caching allocator. Explicitly
# record every CUDA allocation read or written by the native call so a
# tensor produced on another stream cannot be recycled while the
# resident kernel is still using it.
for tensor in (
contiguous,
self.packed_weight,
self.weight_scales,
self.weight_scale,
self.bias,
padded_output,
):
if tensor is not None:
tensor.record_stream(current_stream)
context = resident_context(input.device)
context.forward(
contiguous,
self.packed_weight,
self.weight_scales,
self.weight_scale,
self.bias,
padded_output,
logical_m,
self.in_features,
self.out_features,
)
logical = padded_output[:logical_m]
return logical.view(*input.shape[:-1], self.out_features)
def extra_repr(self) -> str:
return (
f"in_features={self.in_features}, "
f"out_features={self.out_features}, "
f"bias={self.bias is not None}, inference_only=True"
)