[XPU][Fix] Pack device-pointer tables as uint64 to avoid 64-bit address overflow (#35051)

Co-authored-by: roopaksrivastav <roopak.srivastava@intel.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dayananda V
2026-09-10 10:12:18 +08:00
committed by GitHub
co-authored by roopaksrivastav Claude Opus 5
parent 3700c4ee26
commit fd596a474c
11 changed files with 346 additions and 23 deletions
@@ -10,6 +10,8 @@ import torch
import triton
import triton.language as tl
from sglang.kernels.ops.memory.ptr_table import make_ptr_table
def _require_entry_contiguous_dst(
dst: torch.Tensor, entry_start_dim: int, fn_name: str
@@ -578,7 +580,7 @@ def _conv_multi_build_meta(pairs, block_size: int):
]
)
block_start += triton.cdiv(elem, block_size)
meta = torch.tensor(rows, dtype=torch.int64, device=pairs[0][0].device)
meta = make_ptr_table(rows, device=pairs[0][0].device)
return meta, block_start
@@ -0,0 +1,22 @@
"""Device-pointer tables for kernels that address several tensors per launch."""
from __future__ import annotations
from typing import Sequence, Union
import torch
def make_ptr_table(
rows: Union[Sequence[int], Sequence[Sequence[int]]],
device: Union[torch.device, str],
) -> torch.Tensor:
"""Pack ``data_ptr()`` values -- flat, or 2-D with companion columns such as
strides -- into an ``int64`` table a kernel bitcasts back to pointers.
Built unsigned because XPU USM addresses set the top bit, which
``dtype=torch.int64`` rejects while unpacking through ``long long``;
``view`` moves no bits, so kernels keep their signed element type.
Values must be in ``[0, 2**64)``.
"""
return torch.tensor(rows, dtype=torch.uint64, device=device).view(torch.int64)
@@ -24,11 +24,13 @@ import torch
import triton
import triton.language as tl
from sglang.kernels.ops.memory.ptr_table import make_ptr_table
_BLOCK = 1024
class ConvSlotDescriptor(NamedTuple):
ptr: torch.Tensor # [T] int64 base byte-addresses
ptr: torch.Tensor # [T] int64-viewed base byte-addresses (make_ptr_table)
feat: torch.Tensor # [T] int64 per-slot feature length (elements)
layer_stride: torch.Tensor # [T] int64 element stride between layers
slot_stride: torch.Tensor # [T] int64 element stride between slots
@@ -116,7 +118,7 @@ def build_conv_slot_descriptor(tensors: List[torch.Tensor]) -> ConvSlotDescripto
max_feat = max(max_feat, t[0, 0].numel())
to_i64 = lambda xs: torch.tensor(xs, dtype=torch.int64, device=device)
return ConvSlotDescriptor(
ptr=to_i64(ptr),
ptr=make_ptr_table(ptr, device=device),
feat=to_i64(feat),
layer_stride=to_i64(layer_stride),
slot_stride=to_i64(slot_stride),
+3 -3
View File
@@ -489,7 +489,7 @@ class MambaPool:
*physical_conv_shape,
),
dtype=conv_dtype,
device="cuda",
device=self.device,
)
physical_conv_strides = phys.stride()[2:]
window_stride = physical_conv_strides[window_axis]
@@ -771,7 +771,7 @@ class MambaPool:
temporal_state_shape[2],
),
dtype=ssm_dtype,
device="cuda",
device=device,
)
# Cache intermediate conv windows (last K-1 inputs) per draft token
# during target verify.
@@ -839,7 +839,7 @@ class MambaPool:
conv_shape[1],
),
dtype=conv_dtype,
device="cuda",
device=device,
)
for conv_shape in dense_conv_shapes
]
+6
View File
@@ -41,6 +41,10 @@ _LEGACY_CUDA_PREFIXES = ("stress",)
_TEST_KINDS = {"unit", "kernel", "e2e", "accuracy", "perf", "stress"}
# Flat vendor trees. Vendor-only coverage fits no kind above: no XPU/NPU suite
# carries the `-kernel-` infix `kernel` needs, and these launch device work.
_VENDOR_DIRS = {"amd", "mlx", "musa", "npu", "xpu"}
def _defines_testcase(tree: ast.AST) -> bool:
"""True if the file defines unittest classes, statically or via type()."""
@@ -126,6 +130,8 @@ def taxonomy_errors(path: str, registries: list, tree: ast.AST) -> list[str]:
parts = path.split("/")
relative_parts = parts[2:] if parts[:2] == ["test", "registered"] else []
if relative_parts and relative_parts[0] in _VENDOR_DIRS:
return []
if len(relative_parts) < 3 or relative_parts[0] not in _TEST_KINDS:
return [
f"{path}: registered tests must live under "
@@ -10,14 +10,16 @@ from sglang.srt.distributed.device_communicators.custom_all_reduce_utils import
update_environment_variables,
)
from sglang.srt.distributed.parallel_state import (
get_default_distributed_backend,
init_distributed_environment,
initialize_model_parallel,
)
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils import get_device, get_device_count
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.ci.ci_register import register_cuda_ci, register_xpu_ci
register_cuda_ci(est_time=30, stage="base-b", runner_config="2-gpu-large")
register_xpu_ci(est_time=60, suite="nightly-xpu-2-gpu", nightly=True)
NUM_GPUS = 2
@@ -96,9 +98,12 @@ def mixer2_gated_norm_tensor_parallel(
}
)
# initialize distributed
# nccl on CUDA, xccl on XPU, ...; the parameter default is always "nccl".
init_distributed_environment(
world_size=world_size, rank=local_rank, local_rank=local_rank
world_size=world_size,
rank=local_rank,
local_rank=local_rank,
backend=get_default_distributed_backend(device.type),
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
@@ -5,6 +5,9 @@ reference loop that MambaPool.clear_slots / copy_from fall back to.
Covers heterogeneous conv shapes, single- and multi-layer pools, single /
partial / full index sets, int32 indices, and the strided per-slot-envelope
layout used by page-major / unified pools.
Runs on whichever Triton-capable accelerator is present, not CUDA only: the
descriptor packs ``data_ptr()`` values, which only overflows off CUDA (#35047).
"""
import unittest
@@ -16,10 +19,29 @@ from sglang.srt.mem_cache.mamba_slot_fused import (
fused_clear_conv_slots,
fused_copy_conv_slots,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.srt.utils import get_device
from sglang.srt.utils.common import get_device_module
from sglang.test.ci.ci_register import register_cuda_ci, register_xpu_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-small")
register_xpu_ci(est_time=20, suite="stage-b-test-1-gpu-xpu")
# Backends these kernels are verified against; extend as others gain Triton.
TRITON_DEVICES = ("cuda", "xpu")
def _triton_device():
# get_device() raises when the host has no accelerator; importing this
# module must not.
try:
device = get_device()
except RuntimeError:
return None
return device if device in TRITON_DEVICES else None
DEVICE = _triton_device()
CONV_LEN = 3
# Representative hybrid conv-state trailing dims (a couple of KV-projection
@@ -80,10 +102,13 @@ def _ref_copy(convs, src, dst):
t[:, dst] = t[:, src]
@unittest.skipUnless(torch.cuda.is_available(), "fused conv-slot kernels need CUDA")
@unittest.skipUnless(
DEVICE is not None,
f"fused conv-slot kernels need one of {TRITON_DEVICES}",
)
class TestMambaSlotFused(CustomTestCase):
def test_clear_matches_reference(self):
dev = "cuda"
dev = DEVICE
for dims, num_layers, pool in CONFIGS:
for n in sorted({1, pool // 3, pool}): # single / partial / all slots
with self.subTest(dims=dims, num_layers=num_layers, pool=pool, n=n):
@@ -93,7 +118,7 @@ class TestMambaSlotFused(CustomTestCase):
got = [t.clone() for t in base]
_ref_clear(ref, idx)
fused_clear_conv_slots(build_conv_slot_descriptor(got), idx)
torch.cuda.synchronize()
get_device_module().synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
# Cleared slots are exactly zero; the rest is untouched.
@@ -104,7 +129,7 @@ class TestMambaSlotFused(CustomTestCase):
self.assertTrue(torch.equal(g[:, keep], b[:, keep]))
def test_copy_matches_reference(self):
dev = "cuda"
dev = DEVICE
for dims, num_layers, pool in CONFIGS:
with self.subTest(dims=dims, num_layers=num_layers, pool=pool):
base = _make_convs(dims, num_layers, pool, dev, seed=1)
@@ -116,7 +141,7 @@ class TestMambaSlotFused(CustomTestCase):
got = [t.clone() for t in base]
_ref_copy(ref, src, dst)
fused_copy_conv_slots(build_conv_slot_descriptor(got), src, dst)
torch.cuda.synchronize()
get_device_module().synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
@@ -126,7 +151,7 @@ class TestMambaSlotFused(CustomTestCase):
# kernel reads real strides, so it must handle this; the whole envelope
# buffer (including the other streams' bytes in each slot) must be
# bit-exact vs the reference, proving no cross-stream clobber.
dev = "cuda"
dev = DEVICE
num_layers, pool = 2, 48
dims = [128, 256, 6144]
envelope = sum(CONV_LEN * d for d in dims)
@@ -148,7 +173,7 @@ class TestMambaSlotFused(CustomTestCase):
),
idx,
)
torch.cuda.synchronize()
get_device_module().synchronize()
self.assertTrue(torch.equal(ref_buf, got_buf))
# copy on the same strided layout
@@ -164,31 +189,31 @@ class TestMambaSlotFused(CustomTestCase):
src,
dst,
)
torch.cuda.synchronize()
get_device_module().synchronize()
self.assertTrue(torch.equal(ref_buf, got_buf))
def test_empty_indices_is_noop(self):
dev = "cuda"
dev = DEVICE
base = _make_convs(HETERO_DIMS, 1, 16, dev, seed=2)
got = [t.clone() for t in base]
empty = torch.empty(0, dtype=torch.int64, device=dev)
desc = build_conv_slot_descriptor(got)
fused_clear_conv_slots(desc, empty)
fused_copy_conv_slots(desc, empty, empty)
torch.cuda.synchronize()
get_device_module().synchronize()
for b, g in zip(base, got):
self.assertTrue(torch.equal(b, g))
def test_int32_indices_accepted(self):
# deferred-clear/COW indices are staged as int32; the wrappers must upcast.
dev = "cuda"
dev = DEVICE
base = _make_convs(HETERO_DIMS, 1, 32, dev, seed=3)
idx = torch.tensor([1, 5, 9], dtype=torch.int32, device=dev)
ref = [t.clone() for t in base]
got = [t.clone() for t in base]
_ref_clear(ref, idx.long())
fused_clear_conv_slots(build_conv_slot_descriptor(got), idx)
torch.cuda.synchronize()
get_device_module().synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
@@ -2,10 +2,12 @@ from sglang.test.ci.ci_register import (
register_amd_ci,
register_cpu_ci,
register_cuda_ci,
register_xpu_ci,
)
register_cuda_ci(est_time=7, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=7, suite="stage-b-test-1-gpu-small-amd-mi35x")
register_xpu_ci(est_time=20, suite="stage-b-test-1-gpu-xpu")
# The dst layout-contract tests run on CPU (no kernel launch).
register_cpu_ci(est_time=6, suite="base-a-test-cpu")
@@ -16,6 +18,7 @@ import torch
try:
from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
_require_entry_contiguous_dst,
fused_conv_window_scatter_multi,
fused_conv_window_scatter_with_mask,
fused_mamba_state_scatter_with_mask,
)
@@ -23,6 +26,7 @@ try:
_FUSED_IMPORT_ERROR = None
except Exception as e: # pragma: no cover
_require_entry_contiguous_dst = None
fused_conv_window_scatter_multi = None
fused_conv_window_scatter_with_mask = None
fused_mamba_state_scatter_with_mask = None
_FUSED_IMPORT_ERROR = e
@@ -31,6 +35,25 @@ from sglang.srt.mem_cache.layout.page_major import (
build_page_major_mamba_views,
mamba_entry_bytes,
)
from sglang.srt.utils import get_device
from sglang.srt.utils.common import get_device_module
from sglang.test.test_utils import CustomTestCase
# Backends the multi-type scatter is verified against; extend as others gain Triton.
TRITON_DEVICES = ("cuda", "xpu")
def _triton_device():
# get_device() raises when the host has no accelerator; the layout-contract
# cases below still run on the CPU suite.
try:
device = get_device()
except RuntimeError:
return None
return device if device in TRITON_DEVICES else None
DEVICE = _triton_device()
def _ref_scatter(dst, src, dst_indices, src_indices, step_indices):
@@ -365,5 +388,78 @@ class TestMambaStateScatterEnvelopeDst(unittest.TestCase):
torch.testing.assert_close(conv_views[0], expect_conv)
@unittest.skipUnless(
DEVICE is not None,
f"multi-type conv scatter needs one of {TRITON_DEVICES}",
)
class TestFusedConvWindowScatterMulti(CustomTestCase):
"""Multi-type conv scatter must place every conv type correctly; it reaches
them through a host-built device-pointer table (issue #35047)."""
def _run(self, num_types, n2):
# n2 sizes the optional second (interval-crossing track) index set.
torch.manual_seed(11)
dev = DEVICE
layers, slots, batch, steps, dim, km1 = 2, 16, 5, 3, 8, 3
pairs = []
for t in range(num_types):
elems = dim * (t + 1)
pairs.append(
(
torch.randn(
(layers, slots, elems, km1), dtype=torch.bfloat16, device=dev
),
torch.randn(
(layers, batch, steps, elems, km1),
dtype=torch.bfloat16,
device=dev,
),
)
)
g = torch.Generator(device=dev).manual_seed(5)
# One permutation split across the two sets: their dst slots must be
# disjoint, else the two writes to a shared slot race.
perm = torch.randperm(slots, device=dev, generator=g).to(torch.int64)
dst1, dst2 = perm[:batch], perm[batch : batch + n2]
def _steps(n):
step = torch.randint(
0, steps, (n,), device=dev, dtype=torch.int64, generator=g
)
step[0] = -1 # one rejected row per set must be skipped
return step
step1 = _steps(batch)
if n2:
step2 = _steps(n2)
else:
dst2, step2 = None, None
expect = [dst.clone() for dst, _ in pairs]
for exp, (_, src) in zip(expect, pairs):
for dsts, stps in ((dst1, step1), (dst2, step2)):
if stps is None:
continue
valid = stps >= 0
# src row = the row's position within its own index set
rows = torch.arange(stps.numel(), device=dev)[valid]
exp[:, dsts[valid]] = src[:, rows, stps[valid]]
fused_conv_window_scatter_multi(pairs, dst1, step1, dst2, step2)
get_device_module().synchronize()
for i, (exp, (got, _)) in enumerate(zip(expect, pairs)):
torch.testing.assert_close(got, exp, msg=f"conv type {i} mismatch")
def test_single_type(self):
self._run(num_types=1, n2=0)
def test_multi_type(self):
self._run(num_types=3, n2=0)
def test_multi_type_with_track_set(self):
self._run(num_types=2, n2=4)
if __name__ == "__main__": # pragma: no cover
unittest.main()
@@ -26,10 +26,15 @@ from sglang.srt.mem_cache.radix_cache import RadixKey
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.srt.utils import get_device
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
register_cuda_ci(est_time=11, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=9, suite="stage-b-test-1-gpu-small-amd")
register_xpu_ci(est_time=20, suite="stage-b-test-1-gpu-xpu")
def _event_hashes(events):
@@ -168,6 +173,8 @@ class TestMamba(unittest.TestCase):
conv_dim = 5
pool = object.__new__(WindowFirstMambaPool)
# Bypasses __init__, so set the device the allocator reads directly.
pool.device = get_device()
physical, view = pool._allocate_deduplicated_conv_window(
conv_shape=(window_size, conv_dim),
num_mamba_layers=num_mamba_layers,
@@ -0,0 +1,124 @@
"""Device-pointer tables must survive addresses with the top bit set (#35047).
Backends whose addresses stay ``< 2**47`` cannot catch this, so the cases below
spoof a high address onto CPU tensors and run anywhere. The round-trip through
real device memory lives in test/registered/xpu/test_ptr_table.py.
"""
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
import unittest
import torch
from sglang.kernels.ops.memory.ptr_table import make_ptr_table
from sglang.test.test_utils import CustomTestCase
try:
from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
_conv_multi_build_meta,
)
from sglang.srt.mem_cache.mamba_slot_fused import build_conv_slot_descriptor
_IMPORT_ERROR = None
except Exception as e: # triton is not installed on every CPU runner
_conv_multi_build_meta = None
build_conv_slot_descriptor = None
_IMPORT_ERROR = e
# A base address from the issue report; the top bit is set.
_HIGH_BASE = 0xFFFF85ABD4E00000
class _SpoofedPtrTensor(torch.Tensor):
# Only data_ptr() is faked; shape, strides, dtype and device stay real.
_spoofed_ptr = None
def data_ptr(self) -> int:
if self._spoofed_ptr is None:
return super().data_ptr()
return self._spoofed_ptr
def _spoof(t: torch.Tensor, offset: int) -> torch.Tensor:
spoofed = t.as_subclass(_SpoofedPtrTensor)
spoofed._spoofed_ptr = _HIGH_BASE + offset
return spoofed
def _conv_pairs(elems):
# (dst, src) order, as the multi-type scatter takes them.
layers, slots, batch, steps, dim = 2, 8, 4, 3, 1
return [
(
torch.zeros((layers, slots, dim, e), dtype=torch.bfloat16),
torch.zeros((layers, batch, steps, dim, e), dtype=torch.bfloat16),
)
for e in elems
]
def _conv_tensors(feats):
# [layers, slots, conv_len, feat], as build_conv_slot_descriptor takes them.
return [torch.zeros((2, 16, 4, f), dtype=torch.bfloat16) for f in feats]
class TestMakePtrTable(CustomTestCase):
def test_full_range_addresses_round_trip(self):
# 2**63 is the exact value an int64 build rejects.
addrs = [0, 2**47, 2**63 - 1, 2**63, 2**64 - 1]
table = make_ptr_table(addrs, device="cpu")
self.assertEqual(table.dtype, torch.int64)
self.assertEqual(table.view(torch.uint64).tolist(), addrs)
@unittest.skipUnless(_IMPORT_ERROR is None, f"import failed: {_IMPORT_ERROR}")
class TestPtrTableCallSites(CustomTestCase):
"""Built from a spoofed top-bit-set address, each call site's address columns
must survive and its companion columns must match a real-address build."""
def test_conv_multi_meta_table(self):
elems = (128, 64)
real = _conv_pairs(elems)
spoofed = [
(_spoof(dst, 2 * i * 4096), _spoof(src, (2 * i + 1) * 4096))
for i, (dst, src) in enumerate(real)
]
real_meta, real_blocks = _conv_multi_build_meta(real, block_size=64)
meta, blocks = _conv_multi_build_meta(spoofed, block_size=64)
self.assertEqual(meta.dtype, torch.int64)
self.assertEqual(blocks, real_blocks)
# Columns 0/1 are the src/dst base addresses.
addrs = meta.view(torch.uint64)[:, :2].tolist()
self.assertEqual(
addrs, [[src.data_ptr(), dst.data_ptr()] for dst, src in spoofed]
)
self.assertTrue(torch.equal(meta[:, 2:], real_meta[:, 2:]))
def test_conv_slot_descriptor(self):
feats = (128, 6144)
real = _conv_tensors(feats)
spoofed = [_spoof(t, i * 4096) for i, t in enumerate(real)]
real_desc = build_conv_slot_descriptor(real)
desc = build_conv_slot_descriptor(spoofed)
self.assertEqual(desc.ptr.dtype, torch.int64)
self.assertEqual(
desc.ptr.view(torch.uint64).tolist(), [t.data_ptr() for t in spoofed]
)
self.assertEqual(desc.num_layers, real_desc.num_layers)
self.assertEqual(desc.max_feat_blocks, real_desc.max_feat_blocks)
for field in ("feat", "layer_stride", "slot_stride"):
self.assertTrue(
torch.equal(getattr(desc, field), getattr(real_desc, field)), field
)
if __name__ == "__main__":
unittest.main()
+34
View File
@@ -0,0 +1,34 @@
"""Pointer tables built on real XPU memory must round-trip (#35047).
Level Zero / SYCL USM hands out addresses with the top bit set, which an int64
table cannot hold. The spoofed-address cases that run anywhere live in
test/registered/unit/memory/test_ptr_table.py.
"""
from sglang.test.ci.ci_register import register_xpu_ci
register_xpu_ci(est_time=10, suite="stage-b-test-1-gpu-xpu")
import unittest
import torch
from sglang.kernels.ops.memory.ptr_table import make_ptr_table
from sglang.test.test_utils import CustomTestCase
@unittest.skipUnless(torch.xpu.is_available(), "Intel XPU not available")
class TestPtrTableOnDeviceMemory(CustomTestCase):
def test_real_device_pointers_round_trip(self):
ptrs = [
torch.zeros(1024, device="xpu", dtype=torch.bfloat16).data_ptr()
for _ in range(2)
]
table = make_ptr_table(ptrs, device="xpu")
self.assertEqual(table.dtype, torch.int64)
self.assertEqual(table.device.type, "xpu")
self.assertEqual(table.view(torch.uint64).cpu().tolist(), ptrs)
if __name__ == "__main__":
unittest.main()