test: add runtime correctness and benchmark coverage

Co-authored-by: wawahejun <hejunlbbc@gmail.com>
This commit is contained in:
yutianyu 2026-05-02 00:42:36 +08:00
parent ce30e4b195
commit 449075499b
13 changed files with 347 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@

91
tests/bench_all.py Normal file
View File

@ -0,0 +1,91 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PYTHON_DIR = ROOT / "python"
if str(PYTHON_DIR) not in sys.path:
sys.path.insert(0, str(PYTHON_DIR))
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import torch
from operator_runtime import copy, reduce_sum, softmax, vector_add
from operator_runtime.benchmark import cuda_time_ms
from operator_runtime.perf_model import (
estimate_copy,
estimate_reduce_sum,
estimate_softmax,
estimate_vector_add,
)
from operator_runtime.profiler import PerformanceResult
def bench_copy(backend: str) -> PerformanceResult:
src = torch.randn((1 << 20,), dtype=torch.float16, device="cuda")
out = torch.empty_like(src)
runtime = cuda_time_ms(lambda: copy(src, backend=backend))
torch_ms = cuda_time_ms(lambda: out.copy_(src))
bytes_, flops = estimate_copy(src)
return PerformanceResult("copy", backend, str(tuple(src.shape)), str(src.dtype), bytes_, flops, runtime, torch_ms)
def bench_vector_add(backend: str) -> PerformanceResult:
a = torch.randn((1 << 20,), dtype=torch.float16, device="cuda")
b = torch.randn_like(a)
runtime = cuda_time_ms(lambda: vector_add(a, b, backend=backend))
torch_ms = cuda_time_ms(lambda: torch.add(a, b))
bytes_, flops = estimate_vector_add(a)
return PerformanceResult("vector_add", backend, str(tuple(a.shape)), str(a.dtype), bytes_, flops, runtime, torch_ms)
def bench_reduce_sum(backend: str) -> PerformanceResult:
src = torch.randn((1024, 1024), dtype=torch.float32, device="cuda")
runtime = cuda_time_ms(lambda: reduce_sum(src, dim=1, backend=backend))
torch_ms = cuda_time_ms(lambda: torch.sum(src, dim=1))
bytes_, flops = estimate_reduce_sum(src)
return PerformanceResult("reduce_sum", backend, str(tuple(src.shape)), str(src.dtype), bytes_, flops, runtime, torch_ms)
def bench_softmax(backend: str) -> PerformanceResult:
src = torch.randn((1024, 1024), dtype=torch.float32, device="cuda")
runtime = cuda_time_ms(lambda: softmax(src, dim=1, backend=backend))
torch_ms = cuda_time_ms(lambda: torch.softmax(src, dim=1))
bytes_, flops = estimate_softmax(src)
return PerformanceResult("softmax", backend, str(tuple(src.shape)), str(src.dtype), bytes_, flops, runtime, torch_ms)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--backend", default="nvidia")
parser.add_argument("--profile", default=None)
args = parser.parse_args()
if not torch.cuda.is_available():
print("CUDA is required for benchmark", file=sys.stderr)
return 2
rows = [
bench_copy(args.backend),
bench_vector_add(args.backend),
bench_reduce_sum(args.backend),
bench_softmax(args.backend),
]
print("operator backend shape dtype runtime_ms torch_ms speedup GB/s GFLOP/s")
for row in rows:
speedup = 0.0 if row.speedup is None else row.speedup
print(
f"{row.operator} {row.backend} {row.shape} {row.dtype} "
f"{row.runtime_ms:.4f} {row.torch_ms or 0:.4f} {speedup:.2f} "
f"{row.gbytes_per_sec:.2f} {row.gflops_per_sec:.2f}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

1
tests/cases/__init__.py Normal file
View File

@ -0,0 +1 @@

24
tests/cases/copy.py Normal file
View File

@ -0,0 +1,24 @@
from __future__ import annotations
import torch
def correctness_cases():
return [
{"name": "contiguous_1k_fp32", "shape": (1024,), "dtype": torch.float32, "atol": 0, "rtol": 0},
{"name": "contiguous_1k_fp16", "shape": (1024,), "dtype": torch.float16, "atol": 0, "rtol": 0},
]
def api_error_cases():
return [
{"name": "shape_mismatch", "shape": (16,), "out_shape": (8,), "dtype": torch.float32},
{"name": "non_contiguous", "shape": (4, 4), "dtype": torch.float32},
]
def benchmark_cases():
return [
{"name": "contiguous_1m", "shape": (1 << 20,), "dtype": torch.float16},
]

23
tests/cases/reduce_sum.py Normal file
View File

@ -0,0 +1,23 @@
from __future__ import annotations
import torch
def correctness_cases():
return [
{"name": "rowwise_32x128", "shape": (32, 128), "dtype": torch.float32, "atol": 1e-5, "rtol": 1e-5},
]
def api_error_cases():
return [
{"name": "wrong_dim", "shape": (16, 16), "dtype": torch.float32, "dim": 0},
{"name": "wrong_dtype", "shape": (16, 16), "dtype": torch.float16, "dim": 1},
]
def benchmark_cases():
return [
{"name": "rowwise_1024x1024", "shape": (1024, 1024), "dtype": torch.float32},
]

23
tests/cases/softmax.py Normal file
View File

@ -0,0 +1,23 @@
from __future__ import annotations
import torch
def correctness_cases():
return [
{"name": "rowwise_32x128", "shape": (32, 128), "dtype": torch.float32, "atol": 1e-5, "rtol": 1e-5},
]
def api_error_cases():
return [
{"name": "wrong_dim", "shape": (16, 16), "dtype": torch.float32, "dim": 0},
{"name": "wrong_dtype", "shape": (16, 16), "dtype": torch.float16, "dim": 1},
]
def benchmark_cases():
return [
{"name": "rowwise_1024x1024", "shape": (1024, 1024), "dtype": torch.float32},
]

24
tests/cases/vector_add.py Normal file
View File

@ -0,0 +1,24 @@
from __future__ import annotations
import torch
def correctness_cases():
return [
{"name": "contiguous_1k_fp32", "shape": (1024,), "dtype": torch.float32, "atol": 1e-6, "rtol": 1e-6},
{"name": "contiguous_1k_fp16", "shape": (1024,), "dtype": torch.float16, "atol": 1e-3, "rtol": 1e-3},
]
def api_error_cases():
return [
{"name": "shape_mismatch", "shape": (16,), "other_shape": (8,), "dtype": torch.float32},
{"name": "non_contiguous", "shape": (4, 4), "dtype": torch.float32},
]
def benchmark_cases():
return [
{"name": "contiguous_1m", "shape": (1 << 20,), "dtype": torch.float16},
]

24
tests/conftest.py Normal file
View File

@ -0,0 +1,24 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
PYTHON_DIR = ROOT / "python"
if str(PYTHON_DIR) not in sys.path:
sys.path.insert(0, str(PYTHON_DIR))
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
def pytest_addoption(parser):
parser.addoption("--backend", action="store", default=os.environ.get("CAMP_TEST_BACKEND", "nvidia"))
@pytest.fixture
def backend(request) -> str:
return request.config.getoption("--backend")

View File

@ -0,0 +1,4 @@
name: local-gpu
peak_bandwidth_gb_s: 1000
notes: "Placeholder profile. Replace with a measured HBM microbenchmark value."

View File

@ -0,0 +1,38 @@
from __future__ import annotations
import pytest
import torch
from operator_runtime import copy_, reduce_sum, softmax, vector_add
from operator_runtime.testing import require_cuda
def test_copy_rejects_shape_mismatch(backend):
require_cuda()
src = torch.randn((16,), device="cuda")
out = torch.empty((8,), device="cuda")
with pytest.raises(ValueError):
copy_(out, src, backend=backend)
def test_vector_add_rejects_shape_mismatch(backend):
require_cuda()
a = torch.randn((16,), device="cuda")
b = torch.randn((8,), device="cuda")
with pytest.raises(ValueError):
vector_add(a, b, backend=backend)
def test_reduce_sum_rejects_wrong_dim(backend):
require_cuda()
src = torch.randn((16, 16), device="cuda")
with pytest.raises(ValueError):
reduce_sum(src, dim=0, backend=backend)
def test_softmax_rejects_wrong_dtype(backend):
require_cuda()
src = torch.randn((16, 16), dtype=torch.float16, device="cuda")
with pytest.raises(TypeError):
softmax(src, dim=1, backend=backend)

45
tests/test_correctness.py Normal file
View File

@ -0,0 +1,45 @@
from __future__ import annotations
import pytest
import torch
from operator_runtime import copy, reduce_sum, softmax, vector_add
from operator_runtime.testing import assert_close, require_cuda
from tests.cases import copy as copy_cases
from tests.cases import reduce_sum as reduce_sum_cases
from tests.cases import softmax as softmax_cases
from tests.cases import vector_add as vector_add_cases
@pytest.mark.parametrize("case", copy_cases.correctness_cases(), ids=lambda c: c["name"])
def test_copy_correctness(case, backend):
require_cuda()
src = torch.randn(case["shape"], dtype=case["dtype"], device="cuda")
out = copy(src, backend=backend)
assert_close(out, src, atol=case["atol"], rtol=case["rtol"])
@pytest.mark.parametrize("case", vector_add_cases.correctness_cases(), ids=lambda c: c["name"])
def test_vector_add_correctness(case, backend):
require_cuda()
a = torch.randn(case["shape"], dtype=case["dtype"], device="cuda")
b = torch.randn(case["shape"], dtype=case["dtype"], device="cuda")
out = vector_add(a, b, backend=backend)
assert_close(out, a + b, atol=case["atol"], rtol=case["rtol"])
@pytest.mark.parametrize("case", reduce_sum_cases.correctness_cases(), ids=lambda c: c["name"])
def test_reduce_sum_correctness(case, backend):
require_cuda()
src = torch.randn(case["shape"], dtype=case["dtype"], device="cuda")
out = reduce_sum(src, dim=1, backend=backend)
assert_close(out, torch.sum(src, dim=1), atol=case["atol"], rtol=case["rtol"])
@pytest.mark.parametrize("case", softmax_cases.correctness_cases(), ids=lambda c: c["name"])
def test_softmax_correctness(case, backend):
require_cuda()
src = torch.randn(case["shape"], dtype=case["dtype"], device="cuda")
out = softmax(src, dim=1, backend=backend)
assert_close(out, torch.softmax(src, dim=1), atol=case["atol"], rtol=case["rtol"])

View File

@ -0,0 +1,24 @@
from __future__ import annotations
import pytest
import torch
from operator_runtime.ops.vector_add import prepare_vector_add
from operator_runtime.testing import assert_close, require_cuda
def test_prepared_vector_add_reuses_descriptor(backend):
require_cuda()
if backend != "nvidia":
pytest.skip("descriptor lifecycle test targets C ABI backend")
a = torch.randn((1024,), device="cuda")
b = torch.randn((1024,), device="cuda")
out = torch.empty_like(a)
prepared = prepare_vector_add(out, a, b, backend=backend)
try:
prepared.run()
prepared.run()
assert_close(out, a + b, atol=1e-6, rtol=1e-6)
finally:
prepared.destroy()

View File

@ -0,0 +1,25 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
def test_manifest_validator_passes():
root = Path(__file__).resolve().parents[1]
result = subprocess.run(
[
sys.executable,
"tools/validate_operator_manifest.py",
"--ops-root",
"ops",
"--tests-root",
"tests",
],
cwd=root,
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0, result.stderr