Skip to content

Commit e86e86e

Browse files
Merge pull request #27 from flagos-ai/cuda_update
merge
2 parents e75bd19 + b1da0c4 commit e86e86e

5 files changed

Lines changed: 789 additions & 229 deletions

File tree

src/flagsparse/sparse_operations/_common.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Shared imports, dtypes, and helpers for FlagSparse sparse ops."""
22

3+
import statistics
34
import time
45

56
try:
@@ -53,6 +54,7 @@
5354
"_prepare_inputs",
5455
"_prepare_scatter_inputs",
5556
"_benchmark_cuda_op",
57+
"_benchmark_cuda_graph_op",
5658
"cp",
5759
"cpx_sparse",
5860
"time",
@@ -332,3 +334,50 @@ def _benchmark_cuda_op(op, warmup, iters):
332334
cp.cuda.runtime.deviceSynchronize()
333335
elapsed_ms = (time.perf_counter() - start_time) * 1000.0 / iters
334336
return output, elapsed_ms
337+
338+
339+
def _benchmark_cuda_graph_op(
340+
op,
341+
*,
342+
graph_batch=100,
343+
warmup=20,
344+
repeats=10,
345+
capture_setup=None,
346+
):
347+
"""Measure allocation-free CUDA work without Python/FFI launch gaps."""
348+
graph_batch = max(1, int(graph_batch))
349+
warmup = max(0, int(warmup))
350+
repeats = max(1, int(repeats))
351+
352+
torch.cuda.synchronize()
353+
capture_stream = torch.cuda.Stream()
354+
graph = torch.cuda.CUDAGraph()
355+
356+
with torch.cuda.stream(capture_stream):
357+
if capture_setup is not None:
358+
capture_setup()
359+
op()
360+
capture_stream.synchronize()
361+
362+
with torch.cuda.graph(graph, stream=capture_stream):
363+
for _ in range(graph_batch):
364+
op()
365+
capture_stream.synchronize()
366+
367+
with torch.cuda.stream(capture_stream):
368+
for _ in range(warmup):
369+
graph.replay()
370+
capture_stream.synchronize()
371+
372+
samples_ms = []
373+
start = torch.cuda.Event(enable_timing=True)
374+
end = torch.cuda.Event(enable_timing=True)
375+
for _ in range(repeats):
376+
with torch.cuda.stream(capture_stream):
377+
start.record()
378+
graph.replay()
379+
end.record()
380+
end.synchronize()
381+
samples_ms.append(float(start.elapsed_time(end)) / graph_batch)
382+
383+
return statistics.median(samples_ms)

src/flagsparse/sparse_operations/benchmarks.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
_PreparedCusparseNativeGather,
88
SUPPORTED_SCATTER_VALUE_DTYPES,
99
_scatter_dtype_error_message,
10+
_cusparse_native_gather_skip_reason,
1011
_cusparse_spmv,
1112
_make_scatter_selector_matrix,
1213
_pytorch_scatter_impl,
14+
_set_cusparse_stream,
1315
_triton_gather_impl,
1416
_triton_scatter_impl,
1517
)
@@ -33,6 +35,9 @@
3335
from .spsm import benchmark_spsm_case
3436

3537

38+
_GATHER_GRAPH_BATCH = 100
39+
40+
3641
def _cupy_spmv_op_matrix(matrix, op_code):
3742
if op_code == SPMV_OP_NON:
3843
return matrix
@@ -80,21 +85,43 @@ def benchmark_gather_case(
8085
block_size=DEFAULT_GATHER_BLOCK_SIZE,
8186
run_cusparse=True,
8287
):
83-
"""Benchmark Triton vs PyTorch indexing vs native cuSPARSE gather."""
88+
"""Benchmark gather device time with CUDA Graph replay and CUDA events."""
8489
device = torch.device("cuda")
8590
dense_vector = _build_random_dense(dense_size, value_dtype, device)
8691
indices = _build_indices(nnz, dense_size, index_dtype, device, unique=False)
8792

8893
dense_vector, indices, kernel_indices = _prepare_inputs(dense_vector, indices)
8994
expected = dense_vector[indices]
9095

91-
pytorch_op = lambda: dense_vector[indices]
96+
pytorch_out = torch.empty_like(expected)
97+
triton_out = torch.empty_like(expected)
98+
pytorch_op = lambda: torch.index_select(
99+
dense_vector, 0, indices, out=pytorch_out
100+
)
92101
triton_op = lambda: _triton_gather_impl(
93-
dense_vector, kernel_indices, block_size=block_size
102+
dense_vector, kernel_indices, out=triton_out, block_size=block_size
94103
)
95104

96-
pytorch_values, pytorch_ms = _benchmark_cuda_op(pytorch_op, warmup=warmup, iters=iters)
97-
triton_values, triton_ms = _benchmark_cuda_op(triton_op, warmup=warmup, iters=iters)
105+
try:
106+
pytorch_ms = _benchmark_cuda_graph_op(
107+
pytorch_op,
108+
graph_batch=_GATHER_GRAPH_BATCH,
109+
warmup=warmup,
110+
repeats=iters,
111+
)
112+
except Exception as exc:
113+
raise RuntimeError(f"PyTorch CUDA Graph timing failed: {exc}") from exc
114+
try:
115+
triton_ms = _benchmark_cuda_graph_op(
116+
triton_op,
117+
graph_batch=_GATHER_GRAPH_BATCH,
118+
warmup=warmup,
119+
repeats=iters,
120+
)
121+
except Exception as exc:
122+
raise RuntimeError(f"Triton CUDA Graph timing failed: {exc}") from exc
123+
pytorch_values = pytorch_out
124+
triton_values = triton_out
98125

99126
atol, rtol = _tolerance_for_dtype(value_dtype)
100127
triton_match = torch.allclose(triton_values, expected, atol=atol, rtol=rtol)
@@ -109,7 +136,7 @@ def benchmark_gather_case(
109136
cusparse_max_error = None
110137
cusparse_reason = None
111138
if run_cusparse:
112-
skip_reason = _cusparse_baseline_skip_reason(value_dtype)
139+
skip_reason = _cusparse_native_gather_skip_reason(value_dtype)
113140
if skip_reason:
114141
cusparse_reason = skip_reason
115142
else:
@@ -120,9 +147,21 @@ def benchmark_gather_case(
120147
dense_vector, indices, out=cusparse_out
121148
)
122149
cusparse_op = lambda: cusparse_plan.run()
123-
cusparse_values, cusparse_ms = _benchmark_cuda_op(
124-
cusparse_op, warmup=warmup, iters=iters
125-
)
150+
try:
151+
cusparse_ms = _benchmark_cuda_graph_op(
152+
cusparse_op,
153+
graph_batch=_GATHER_GRAPH_BATCH,
154+
warmup=warmup,
155+
repeats=iters,
156+
capture_setup=lambda: _set_cusparse_stream(
157+
cusparse_plan.lib, cusparse_plan.handle, strict=True
158+
),
159+
)
160+
except Exception as exc:
161+
raise RuntimeError(
162+
f"cuSPARSE CUDA Graph timing failed: {exc}"
163+
) from exc
164+
cusparse_values = cusparse_out
126165
cusparse_match = torch.allclose(
127166
cusparse_values, expected, atol=atol, rtol=rtol
128167
)
@@ -154,13 +193,15 @@ def benchmark_gather_case(
154193
"index_dtype": str(index_dtype),
155194
"warmup": warmup,
156195
"iters": iters,
196+
"kernel_graph_batch": _GATHER_GRAPH_BATCH,
157197
},
158198
"performance": {
159199
"pytorch_ms": pytorch_ms,
160200
"triton_ms": triton_ms,
161201
"cusparse_ms": cusparse_ms,
162202
"triton_speedup_vs_pytorch": triton_speedup_vs_pytorch,
163203
"triton_speedup_vs_cusparse": triton_speedup_vs_cusparse,
204+
"kernel_timing_method": "cuda_graph_event_amortized_device_estimate",
164205
},
165206
"verification": {
166207
"triton_match_pytorch": triton_match,

src/flagsparse/sparse_operations/gather_scatter.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ def _validate_gather_value_dtype(dense_vector, op_name):
298298
_CUSPARSE_LIB_LOAD_ERROR = None
299299

300300

301+
def _cusparse_native_gather_skip_reason(value_dtype):
302+
if value_dtype == torch.bfloat16:
303+
return "bfloat16 is not supported by native cusparseGather; skipped"
304+
return None
305+
306+
301307
def _cuda_data_type_from_torch(torch_dtype):
302308
mapping = {
303309
torch.float16: _CUDA_R_16F,
@@ -388,27 +394,33 @@ def _check_cusparse_status(status, op_name):
388394
raise RuntimeError(f"{op_name} failed with cuSPARSE status {int(status)}")
389395

390396

391-
def _set_cusparse_stream(lib, handle):
397+
def _set_cusparse_stream(lib, handle, *, strict=False):
392398
if not hasattr(lib, "cusparseSetStream"):
399+
if strict:
400+
raise RuntimeError("Loaded cuSPARSE library does not export cusparseSetStream")
393401
return
394402
try:
395403
stream = torch.cuda.current_stream()
396404
stream_ptr = getattr(stream, "cuda_stream", None)
397405
if stream_ptr is None:
406+
if strict:
407+
raise RuntimeError("Could not obtain the current CUDA stream pointer")
398408
return
399409
_check_cusparse_status(
400410
lib.cusparseSetStream(handle, ctypes.c_void_p(int(stream_ptr))),
401411
"cusparseSetStream",
402412
)
403413
except Exception:
414+
if strict:
415+
raise
404416
return
405417

406418

407419
class _PreparedCusparseNativeGather:
408420
def __init__(self, dense_vector, indices, out=None):
409421
dense_vector, indices, _ = _prepare_inputs(dense_vector, indices)
410422
_validate_gather_value_dtype(dense_vector, "cusparse_native_gather")
411-
skip_reason = _cusparse_baseline_skip_reason(dense_vector.dtype)
423+
skip_reason = _cusparse_native_gather_skip_reason(dense_vector.dtype)
412424
if skip_reason:
413425
raise RuntimeError(skip_reason)
414426

tests/test_gather.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
import csv
33
import math
44
import os
5+
import sys
6+
from pathlib import Path
7+
8+
9+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
10+
SRC_ROOT = PROJECT_ROOT / "src"
11+
if SRC_ROOT.is_dir():
12+
sys.path.insert(0, str(SRC_ROOT))
513

614
import torch
715

@@ -18,6 +26,7 @@
1826
DEFAULT_INDEX_DTYPES = "int32,int64"
1927
WARMUP = 20
2028
ITERS = 200
29+
KERNEL_GRAPH_BATCH = 100
2130

2231

2332
def _fmt_ms(value):
@@ -82,8 +91,8 @@ def _parse_cases(raw):
8291
left, right = item.split(":", 1)
8392
dense_size = int(left)
8493
nnz = int(right)
85-
if dense_size < 0 or nnz < 0:
86-
raise ValueError(f"case values must be non-negative: {item}")
94+
if dense_size <= 0 or nnz <= 0:
95+
raise ValueError(f"case values must be positive: {item}")
8796
pairs.append((dense_size, nnz))
8897
if not pairs:
8998
raise ValueError("case list is empty")
@@ -172,7 +181,7 @@ def _print_header():
172181
print("-" * 196)
173182
print(
174183
f"{'ValueReq':>14} {'ValueEff':>18} {'Index':>6} {'Dense':>10} {'NNZ':>10} "
175-
f"{'IFB':>4} {'PT(ms)':>10} {'FS(ms)':>10} {'CS(ms)':>10} "
184+
f"{'IFB':>4} {'FS(ms)':>10} {'PT(ms)':>10} {'CS(ms)':>10} "
176185
f"{'FS/PT':>8} {'FS/CS':>8} {'Status':>6} {'Err(FS)':>12} {'Err(CS)':>12}"
177186
)
178187
print("-" * 196)
@@ -182,7 +191,7 @@ def _print_row(row):
182191
print(
183192
f"{row['value_dtype_req']:>14} {row['value_dtype_compute']:>18} {row['index_dtype']:>6} "
184193
f"{row['dense_size']:>10,d} {row['nnz']:>10,d} {str(row['index_fallback_applied']):>4} "
185-
f"{_fmt_ms(row['pytorch_ms']):>10} {_fmt_ms(row['triton_ms']):>10} {_fmt_ms(row['cusparse_ms']):>10} "
194+
f"{_fmt_ms(row['triton_ms']):>10} {_fmt_ms(row['pytorch_ms']):>10} {_fmt_ms(row['cusparse_ms']):>10} "
186195
f"{_fmt_speedup(row['triton_speedup_vs_pytorch']):>8} {_fmt_speedup(row['triton_speedup_vs_cusparse']):>8} "
187196
f"{row['status']:>6} {_fmt_err(row['triton_max_error']):>12} {_fmt_err(row['cusparse_max_error']):>12}"
188197
)
@@ -201,9 +210,11 @@ def run_cli(args):
201210
print("=" * 180)
202211
print("FLAGSPARSE GATHER BENCHMARK/VALIDATION")
203212
print("=" * 180)
213+
print(f"FlagSparse source: {Path(ast.__file__).resolve()}")
204214
print(f"GPU: {torch.cuda.get_device_name(0)}")
205215
print(
206216
f"Warmup: {args.warmup} | Iterations: {args.iters} | "
217+
f"Kernel graph batch: {KERNEL_GRAPH_BATCH} | "
207218
f"index_fallback_policy: {args.index_fallback_policy}"
208219
)
209220
print()
@@ -242,6 +253,18 @@ def run_cli(args):
242253
verify = result["verification"]
243254
params = result["parameters"]
244255
backend = result["backend_status"]
256+
timing_method = perf.get("kernel_timing_method")
257+
graph_batch = params.get("kernel_graph_batch")
258+
if timing_method != "cuda_graph_event_amortized_device_estimate":
259+
raise RuntimeError(
260+
"loaded benchmark_gather_case does not provide CUDA Graph timing; "
261+
f"flagsparse was loaded from {Path(ast.__file__).resolve()}"
262+
)
263+
if graph_batch != KERNEL_GRAPH_BATCH:
264+
raise RuntimeError(
265+
"unexpected gather graph batch: "
266+
f"expected {KERNEL_GRAPH_BATCH}, got {graph_batch}"
267+
)
245268
status = _status_from_result(verify)
246269
if status != "PASS":
247270
failed_cases += 1
@@ -263,6 +286,8 @@ def run_cli(args):
263286
"cusparse_ms": perf.get("cusparse_ms"),
264287
"triton_speedup_vs_pytorch": perf.get("triton_speedup_vs_pytorch"),
265288
"triton_speedup_vs_cusparse": perf.get("triton_speedup_vs_cusparse"),
289+
"kernel_timing_method": timing_method,
290+
"kernel_graph_batch": graph_batch,
266291
"triton_match_pytorch": verify.get("triton_match_pytorch"),
267292
"cusparse_match_pytorch": verify.get("cusparse_match_pytorch"),
268293
"triton_max_error": verify.get("triton_max_error"),
@@ -285,6 +310,8 @@ def run_cli(args):
285310
)
286311
except Exception as exc:
287312
failed_cases += 1
313+
error_text = f"{exc.__class__.__name__}: {exc}"
314+
print(f"\nERROR [{case_id}]: {error_text}")
288315
row = {
289316
"case_id": case_id,
290317
"gpu": torch.cuda.get_device_name(0),
@@ -301,12 +328,14 @@ def run_cli(args):
301328
"cusparse_ms": None,
302329
"triton_speedup_vs_pytorch": None,
303330
"triton_speedup_vs_cusparse": None,
331+
"kernel_timing_method": "cuda_graph_event_amortized_device_estimate",
332+
"kernel_graph_batch": KERNEL_GRAPH_BATCH,
304333
"triton_match_pytorch": None,
305334
"cusparse_match_pytorch": None,
306335
"triton_max_error": None,
307336
"cusparse_max_error": None,
308-
"cusparse_unavailable_reason": str(exc),
309-
"index_fallback_reason": str(exc),
337+
"cusparse_unavailable_reason": error_text,
338+
"index_fallback_reason": error_text,
310339
"status": "ERROR",
311340
}
312341
summary_rows.append(row)
@@ -334,6 +363,8 @@ def run_cli(args):
334363
"cusparse_ms",
335364
"triton_speedup_vs_pytorch",
336365
"triton_speedup_vs_cusparse",
366+
"kernel_timing_method",
367+
"kernel_graph_batch",
337368
"triton_match_pytorch",
338369
"cusparse_match_pytorch",
339370
"triton_max_error",

0 commit comments

Comments
 (0)