Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

606 lines
23 KiB
Python

"""Unit tests for the breakable CUDA graph core (Phase 1).
Captures a tiny ``Linear -> eager break -> Linear`` forward with
:class:`BreakableCapture`, mutates the static input buffer, replays, and asserts
the replayed output matches an eager recompute. This exercises the load-bearing
invariants in isolation -- segment splitting, the eager break handoff, shared
mempool address stability -- without touching the model or the hot path.
"""
import os
import sys
import unittest
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from ci_system.ci_register import register_cuda_ci # noqa: E402
register_cuda_ci(est_time=15, suite="runtime-1gpu")
from types import SimpleNamespace # noqa: E402
import torch # noqa: E402
from tokenspeed.runtime.execution.breakable_cuda_graph import ( # noqa: E402
BreakableCapture,
active_forward,
break_here,
break_point,
current_forward_ctx,
is_breakable_capture_active,
scrub_padding_tail,
slice_to_real_tokens,
)
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestBreakableCudaGraph(unittest.TestCase):
def setUp(self):
torch.manual_seed(0)
self.dev = "cuda"
self.dtype = torch.float32
self.n, self.d = 8, 16
self.w1 = torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
self.w2 = torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
# Static input buffer: graphs read this address; live inputs are copied in.
self.x_static = torch.zeros(self.n, self.d, device=self.dev, dtype=self.dtype)
def _eager(self, x):
"""Reference forward: Linear -> relu (the "break" op) -> Linear."""
h = x @ self.w1
h = torch.relu(h)
return h @ self.w2
def _build_capture(self):
"""Capture the same forward with relu as an eager break."""
cap = BreakableCapture()
def forward():
h = self.x_static @ self.w1
# Break-output buffer, allocated in-segment so it is pool-pinned.
dst = torch.empty_like(h)
h = break_here(torch.relu, dst, h)
return h @ self.w2
# Warm up (cublas workspace / lazy init) before capture.
for _ in range(3):
forward()
torch.cuda.synchronize()
with cap:
captured_out = forward()
return cap, captured_out
def test_break_here_passthrough_when_inactive(self):
self.assertFalse(is_breakable_capture_active())
h = torch.randn(self.n, self.d, device=self.dev, dtype=self.dtype)
dst = torch.empty_like(h)
out = break_here(torch.relu, dst, h)
self.assertIs(out, dst)
torch.testing.assert_close(out, torch.relu(h))
def test_segments_split_at_break(self):
cap, _ = self._build_capture()
# Two graph segments (before/after relu) + one eager break = 3.
self.assertEqual(cap.num_segments, 3)
def test_replay_matches_eager(self):
cap, captured_out = self._build_capture()
for trial in range(5):
new_x = torch.randn(self.n, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
cap.replay()
torch.cuda.synchronize()
torch.testing.assert_close(
captured_out, self._eager(new_x), msg=f"trial {trial}"
)
def test_multiple_breaks_chain(self):
"""Many breaks (like a deep transformer) must chain correctly."""
depth = 6
ws = [
torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
for _ in range(depth + 1)
]
def eager(x):
h = x @ ws[0]
for i in range(depth):
h = torch.relu(h) # the "break" op
h = h @ ws[i + 1]
return h
cap = BreakableCapture()
def forward():
h = self.x_static @ ws[0]
for i in range(depth):
dst = torch.empty_like(h)
h = break_here(torch.relu, dst, h)
h = h @ ws[i + 1]
return h
for _ in range(3):
forward()
torch.cuda.synchronize()
with cap:
captured_out = forward()
# depth breaks => depth+1 graph segments + depth eager breaks.
self.assertEqual(cap.num_segments, 2 * depth + 1)
for trial in range(4):
new_x = torch.randn(self.n, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
cap.replay()
torch.cuda.synchronize()
torch.testing.assert_close(captured_out, eager(new_x), msg=f"trial {trial}")
def test_nested_capture_rejected(self):
with BreakableCapture():
with self.assertRaises(RuntimeError):
with BreakableCapture():
pass
def test_scrub_padding_tail(self):
# Zeros [num_real:] in place across tensors; skips None; no-op when unpadded.
t1 = torch.ones(6, 3, device=self.dev, dtype=self.dtype)
t2 = torch.ones(6, device=self.dev, dtype=self.dtype)
scrub_padding_tail(4, t1, None, t2)
self.assertTrue(bool((t1[:4] == 1).all()) and bool((t1[4:] == 0).all()))
self.assertTrue(bool((t2[:4] == 1).all()) and bool((t2[4:] == 0).all()))
# Unpadded (count == rows): untouched.
t3 = torch.ones(4, 3, device=self.dev, dtype=self.dtype)
scrub_padding_tail(4, t3)
self.assertTrue(bool((t3 == 1).all()))
def test_slice_to_real_tokens(self):
"""Leading [:num_real] per tensor in order; None and unpadded pass through."""
a = torch.arange(6, device=self.dev)
b = torch.arange(6, device=self.dev).view(6, 1)
c = torch.arange(4, device=self.dev) # already real length
ra, rn, rb, rc = slice_to_real_tokens(4, a, None, b, c)
self.assertEqual(ra.shape[0], 4)
self.assertEqual(rb.shape[0], 4)
self.assertIsNone(rn)
self.assertIs(rc, c) # no-op: not padded
torch.testing.assert_close(ra, torch.arange(4, device=self.dev))
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestBucketedCapture(unittest.TestCase):
"""Per-token-bucket lazy capture with input padding (what PrefillGraph does).
A token-shaped inner forward whose per-layer "attention" runs as an eager
break is captured once per padded token bucket and replayed for any real token
count <= bucket. Mirrors the runner's mechanics directly on BreakableCapture so
the bucketing + padding + replay-parity invariant is unit-tested in isolation.
"""
def setUp(self):
torch.manual_seed(1)
self.dev, self.dtype = "cuda", torch.float32
self.d, self.depth = 16, 3
self.buckets = [4, 8, 16]
self.ws = [
torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
for _ in range(self.depth + 1)
]
# Persistent static input buffer (max bucket); graph reads this address.
self.x_static = torch.zeros(
max(self.buckets), self.d, device=self.dev, dtype=self.dtype
)
self._captures: dict = {}
self._outputs: dict = {}
self._pool = None
def _inner(self, n):
"""Inner forward over the leading ``n`` rows of the static buffer."""
h = self.x_static[:n] @ self.ws[0]
for i in range(self.depth):
dst = torch.empty_like(h)
h = break_here(torch.relu, dst, h) # per-token "attention" break
h = h @ self.ws[i + 1]
return h
def _eager(self, x):
h = x @ self.ws[0]
for i in range(self.depth):
h = torch.relu(h)
h = h @ self.ws[i + 1]
return h
def _run_bucketed(self, n):
"""Pad ``n`` up to a bucket, lazily capture/replay, return output[:n]."""
idx = next(i for i, b in enumerate(self.buckets) if b >= n)
bucket = self.buckets[idx]
cap = self._captures.get(bucket)
if cap is None:
for _ in range(2): # warmup
self._inner(bucket)
torch.cuda.synchronize()
cap = BreakableCapture(pool=self._pool)
with cap:
out = self._inner(bucket)
self._pool = self._pool or cap.pool
cap.replay() # capture doesn't execute; populate `out`
self._captures[bucket], self._outputs[bucket] = cap, out
else:
cap.replay()
return self._outputs[bucket][:n], bucket
def test_replay_matches_eager_across_buckets(self):
captured = set()
for n in [3, 4, 5, 8, 11, 16, 1]:
new_x = torch.randn(n, self.d, device=self.dev, dtype=self.dtype)
self.x_static.zero_() # scrub the padded tail
self.x_static[:n].copy_(new_x)
out, bucket = self._run_bucketed(n)
captured.add(bucket)
torch.cuda.synchronize()
torch.testing.assert_close(
out, self._eager(new_x), msg=f"n={n}", rtol=1e-4, atol=1e-4
)
# First sighting of each distinct bucket captured once; later n replay.
self.assertEqual(set(self._captures), {4, 8, 16})
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestBreakPointAndAmbientCtx(unittest.TestCase):
"""The ``@break_point`` decorator + ambient live-context rebind.
Exercises the two properties the model refactor relies on: (1) a decorated
method runs as an eager break under capture / a direct call otherwise, with
its output buffer sized from the named ``out`` arg; (2) the ForwardContext a
break reads is rebound to the LIVE ambient context at replay, so a graph
captured with a dummy ctx replays correctly against a different live ctx.
"""
def setUp(self):
torch.manual_seed(2)
self.dev, self.dtype = "cuda", torch.float32
self.d = 16
self.w0 = torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
self.w1 = torch.randn(self.d, self.d, device=self.dev, dtype=self.dtype)
self.x_static = torch.zeros(8, self.d, device=self.dev, dtype=self.dtype)
def test_passthrough_runs_method_when_not_capturing(self):
"""Off the capture path @break_point always runs the method.
Including side effects and 0-row inputs -- the decorator never silently
skips a method; 0-row / idle handling is each model's own explicit
``if hidden_states.shape[0] == 0`` guard.
"""
calls = []
class M:
@break_point
def forward(self, x, ctx):
calls.append(x.shape[0])
return x * ctx.scale
m = M()
self.assertFalse(is_breakable_capture_active())
x = torch.randn(4, self.d, device=self.dev, dtype=self.dtype)
out = m.forward(x, SimpleNamespace(scale=3.0)) # direct call
torch.testing.assert_close(out, x * 3.0)
# 0 rows: the method still runs and its output (not the input) is returned.
empty = torch.zeros(0, self.d, device=self.dev, dtype=self.dtype)
out0 = m.forward(empty, SimpleNamespace(scale=9.0))
torch.testing.assert_close(out0, empty * 9.0)
self.assertEqual(calls, [4, 0]) # method ran for both, including 0 rows
def test_ambient_ctx_rebinds_at_replay(self):
class M:
def __init__(self, w):
self.w = w
@break_point
def forward(self, x, ctx):
# Reads the (live) ctx; output [tokens, d] matches arg ``x``.
return torch.relu(x @ self.w) * ctx.scale
m = M(self.w1)
dummy = SimpleNamespace(scale=2.0) # capture-time ctx
live = SimpleNamespace(scale=5.0) # replay-time ctx
def outer():
h = self.x_static @ self.w0 # captured graph segment
return m.forward(h, dummy) # eager break reading the ambient ctx
for _ in range(3): # warmup
with active_forward(dummy):
outer()
torch.cuda.synchronize()
cap = BreakableCapture()
with active_forward(dummy):
with cap:
captured = outer()
# 1 break => 2 graph segments + 1 eager break.
self.assertEqual(cap.num_segments, 3)
new_x = torch.randn(8, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
with active_forward(live): # replay against a DIFFERENT live ctx
cap.replay()
torch.cuda.synchronize()
# The break must have used live.scale (5.0), not the captured dummy (2.0).
expected = torch.relu((new_x @ self.w0) @ self.w1) * 5.0
torch.testing.assert_close(captured, expected, rtol=1e-4, atol=1e-4)
def test_break_reads_live_scalar_off_ambient_not_frozen_arg(self):
"""Scalar args freeze at capture; live values must come off the ambient ctx.
Mirrors the hybrid attention-backend pattern: only the ambient ctx is
rebound at replay, so a break needing a live scalar (forward_mode, bs)
reads current_forward_ctx(), never its own frozen arg.
"""
seen = {}
class M:
@break_point
def forward(self, x, frozen_mode):
amb = current_forward_ctx()
seen["frozen"] = frozen_mode
seen["live"] = amb.mode
return x * amb.mult
m = M()
dummy = SimpleNamespace(mode="EXTEND", mult=1.0) # capture-time ctx
live = SimpleNamespace(mode="MIXED", mult=4.0) # replay-time ctx
def outer():
h = self.x_static @ self.w0
return m.forward(h, frozen_mode="EXTEND") # frozen scalar arg
for _ in range(3):
with active_forward(dummy):
outer()
torch.cuda.synchronize()
cap = BreakableCapture()
with active_forward(dummy):
with cap:
captured = outer()
new_x = torch.randn(8, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
with active_forward(live): # replay against a DIFFERENT live ctx
cap.replay()
torch.cuda.synchronize()
# The positional/kw scalar arg stayed frozen at capture time...
self.assertEqual(seen["frozen"], "EXTEND")
# ...but the ambient read tracked the live ctx (mode + multiplier).
self.assertEqual(seen["live"], "MIXED")
torch.testing.assert_close(
captured, new_x @ self.w0 * 4.0, rtol=1e-4, atol=1e-4
)
def test_break_point_computed_out_shape(self):
"""A NARROW break whose output shape matches no input (deepseek_v3 MLA-like)."""
d2 = self.d // 2
wv = torch.randn(self.d, d2, device=self.dev, dtype=self.dtype)
class M:
@break_point # out-shape (d2 != input d) inferred from the actual output
def attn(self, x): # output last-dim d2 != input last-dim d
return torch.relu(x) @ wv
m = M()
def outer():
h = self.x_static @ self.w0 # captured segment
return m.attn(h) # narrow break, computed output shape
for _ in range(3):
outer()
torch.cuda.synchronize()
cap = BreakableCapture()
with cap:
captured = outer()
self.assertEqual(tuple(captured.shape), (self.x_static.size(0), d2))
new_x = torch.randn(8, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
cap.replay()
torch.cuda.synchronize()
expected = torch.relu(new_x @ self.w0) @ wv
torch.testing.assert_close(captured, expected, rtol=1e-4, atol=1e-4)
def test_nested_break_inner_passes_through(self):
"""A broader @break_point overrides a nested one.
Capture is inactive while the outer break runs eagerly, so an inner
break called inside it passes straight through -- exactly one break.
"""
seen = {"inner_active": None}
class M:
@break_point
def inner(self, x): # would-be default backend break
seen["inner_active"] = is_breakable_capture_active()
return torch.relu(x)
@break_point
def outer(self, x): # broader override break
return self.inner(x) @ self_w1 # noqa: F821
self_w1 = self.w1
m = M()
def fwd():
h = self.x_static @ self.w0 # captured segment
return m.outer(h) # broad break; inner passes through
for _ in range(3):
fwd()
torch.cuda.synchronize()
cap = BreakableCapture()
with cap:
captured = fwd()
# Exactly one break (outer) => 2 graph segments + 1 break = 3.
self.assertEqual(cap.num_segments, 3)
self.assertIs(seen["inner_active"], False) # inner saw inactive capture
new_x = torch.randn(8, self.d, device=self.dev, dtype=self.dtype)
self.x_static.copy_(new_x)
cap.replay()
torch.cuda.synchronize()
expected = torch.relu(new_x @ self.w0) @ self.w1
torch.testing.assert_close(captured, expected, rtol=1e-4, atol=1e-4)
class TestPrefillTokenBuckets(unittest.TestCase):
"""Pure-function tests for the prefill-graph token-bucket schedule (no GPU)."""
@staticmethod
def _cfg(**overrides):
base = dict(
prefill_graph_max_tokens=2048,
disable_prefill_graph=False,
chunked_prefill_size=2048,
prefill_graph_capture_sizes=None,
)
base.update(overrides)
return SimpleNamespace(**base)
def test_disabled(self):
from tokenspeed.runtime.execution.prefill_graph import (
get_prefill_token_buckets,
)
self.assertEqual(
get_prefill_token_buckets(self._cfg(prefill_graph_max_tokens=0)), []
)
self.assertEqual(
get_prefill_token_buckets(self._cfg(disable_prefill_graph=True)), []
)
def test_clamped_to_chunk(self):
from tokenspeed.runtime.execution.prefill_graph import (
get_prefill_token_buckets,
)
# No bucket above the chunk (see get_prefill_token_buckets for why).
buckets = get_prefill_token_buckets(
self._cfg(prefill_graph_max_tokens=8192, chunked_prefill_size=2048)
)
self.assertEqual(buckets[-1], 2048)
def test_relative_ladder_properties(self):
from tokenspeed.runtime.execution.prefill_graph import (
get_prefill_token_buckets,
)
# Gaps bounded relatively (size/8) and absolutely (512); exact top; increasing.
for max_tokens in (8192, 2048, 1500):
buckets = get_prefill_token_buckets(
self._cfg(
prefill_graph_max_tokens=max_tokens,
chunked_prefill_size=max_tokens,
)
)
self.assertEqual(buckets[-1], max_tokens)
self.assertEqual(buckets, sorted(set(buckets)))
gaps = [b2 - b1 for b1, b2 in zip(buckets, buckets[1:])]
for b1, g in zip(buckets, gaps):
self.assertLessEqual(g, 512, f"cap violated at {b1}")
if b1 >= 256:
self.assertLessEqual(g, max(b1 // 8, 16), f"relative bound at {b1}")
def test_explicit_capture_sizes(self):
from tokenspeed.runtime.execution.prefill_graph import (
get_prefill_token_buckets,
)
# Explicit list overrides the ladder; clamped to max_tokens (always included).
buckets = get_prefill_token_buckets(
self._cfg(prefill_graph_capture_sizes=[256, 1024, 4096])
)
self.assertEqual(buckets, [256, 1024, 2048])
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestWeakRefTensor(unittest.TestCase):
"""The non-owning-view op behind break-closure weak refs."""
def test_alias_without_ownership(self):
from tokenspeed.runtime.execution.breakable_cuda_graph import weak_ref_tensor
x = torch.arange(12, device="cuda", dtype=torch.float32).view(3, 4)[:, 1:]
w = weak_ref_tensor(x)
if w is x: # identity fallback (no C++ toolchain) -- still correct
self.skipTest("weak_ref extension unavailable; identity fallback")
# Aliases the same memory (incl. strides), sees writes, owns nothing.
self.assertEqual(w.data_ptr(), x.data_ptr())
self.assertEqual(w.stride(), x.stride())
x.fill_(3.0)
torch.cuda.synchronize()
self.assertTrue(bool((w == 3.0).all()))
# Non-tensor / CPU passthrough.
self.assertIsNone(weak_ref_tensor(None))
cpu = torch.ones(2)
self.assertIs(weak_ref_tensor(cpu), cpu)
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestPoolReuseAcrossCaptures(unittest.TestCase):
"""Graph-pool memory must NOT grow per capture (the dense-ladder enabler).
Allocator pool blocks are stream-keyed, so all BreakableCaptures must share
one capture stream (class-level default) -- a fresh stream per capture makes
pool memory grow with the SUM of bucket sizes instead of the max.
"""
@staticmethod
def _pool_mb():
return (
sum(
s["total_size"]
for s in torch.cuda.memory_snapshot()
if s.get("segment_pool_id", (0, 0)) != (0, 0)
)
/ 2**20
)
def test_same_and_smaller_captures_reuse_pool(self):
d, d2, depth = 512, 2048, 4
w1 = torch.randn(d, d2, device="cuda")
w2 = torch.randn(d2, d, device="cuda")
xbuf = torch.zeros(4096, d, device="cuda")
def fwd(n):
x = xbuf[:n]
for _ in range(depth):
h = x @ w1
dst = torch.empty_like(h)
h = break_here(torch.relu, dst, h)
x = h @ w2
return x
pool = torch.cuda.graph_pool_handle()
caps = []
deltas = []
for n in (4096, 2048, 4096):
for _ in range(2):
fwd(n)
torch.cuda.synchronize()
before = self._pool_mb()
cap = BreakableCapture(pool=pool) # shared default capture stream
with cap:
out = fwd(n)
cap.replay()
torch.cuda.synchronize()
caps.append((cap, out))
deltas.append(self._pool_mb() - before)
# First capture claims ~peak-live; later ones must reuse it (small allowance).
self.assertLess(deltas[1], max(2.0, deltas[0] * 0.1), f"deltas={deltas}")
self.assertLess(deltas[2], max(2.0, deltas[0] * 0.1), f"deltas={deltas}")
# Replays still valid after cross-capture reuse.
for cap, _ in caps:
cap.replay()
torch.cuda.synchronize()
if __name__ == "__main__":
unittest.main()