forked from tile-ai/TileOPs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_reduce_arithmetic_conformance.py
More file actions
151 lines (128 loc) · 4.89 KB
/
Copy pathtest_reduce_arithmetic_conformance.py
File metadata and controls
151 lines (128 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Spec-conformance tests for arithmetic reductions.
Covers ``SumFwdOp``, ``MeanFwdOp``, ``AmaxFwdOp``, ``AminFwdOp`` against the
PyTorch reference (``torch.sum`` / ``torch.mean`` / ``torch.amax`` /
``torch.amin``) across the three ``dim`` shapes the manifest signature
declares — ``int``, ``tuple[int, ...]``, ``None`` — and both ``keepdim``
values.
These tests assert the output shape matches PyTorch (in particular, a 0-D
tensor for ``dim=None, keepdim=False``) and that the numerics match within
standard reduction tolerances. Once green, the corresponding manifest
entries can flip from ``status: spec-only`` to ``status: implemented``
in a separate manifest-only PR per the trust model.
"""
from __future__ import annotations
from tileops.utils import get_backend_name
DEVICE = get_backend_name()
from typing import Callable
import pytest
import torch
from tileops.ops.reduction.reduce import (
AmaxFwdOp,
AminFwdOp,
MeanFwdOp,
SumFwdOp,
)
# (op_cls, torch_fn) pairs.
_OP_CASES: list[tuple[type, Callable]] = [
(SumFwdOp, torch.sum),
(MeanFwdOp, torch.mean),
(AmaxFwdOp, torch.amax),
(AminFwdOp, torch.amin),
]
_SHAPE = (4, 8, 256)
def _tol(dtype: torch.dtype) -> dict:
# Reduce kernels accumulate in fp32 and only narrow at the boundary, so
# half-precision tolerances can stay close to the unit in the last place
# of the storage dtype rather than the looser 1e-2 default.
if dtype == torch.float32:
return {"atol": 1e-4, "rtol": 1e-4}
return {"atol": 1e-3, "rtol": 1e-3}
def _ref(torch_fn: Callable, x: torch.Tensor, dim, keepdim: bool) -> torch.Tensor:
"""PyTorch ground-truth reference. Normalizes ``dim=None`` to all dims
so the same call shape works for sum/mean (which accept None directly)
and amax/amin (which require an explicit dim list)."""
if dim is None:
dims = list(range(x.ndim))
elif isinstance(dim, tuple):
dims = list(dim)
else:
dims = dim
return torch_fn(x.float(), dim=dims, keepdim=keepdim).to(x.dtype)
@pytest.mark.smoke
@pytest.mark.parametrize("op_cls, torch_fn", _OP_CASES, ids=[c[0].__name__ for c in _OP_CASES])
@pytest.mark.parametrize(
"dim",
[
pytest.param(-1, id="dim=int"),
pytest.param((0, 2), id="dim=tuple"),
pytest.param(None, id="dim=None"),
],
)
@pytest.mark.parametrize("keepdim", [False, True], ids=["keepdim=False", "keepdim=True"])
@pytest.mark.parametrize(
"dtype",
[torch.float16, torch.bfloat16, torch.float32],
ids=["fp16", "bf16", "fp32"],
)
def test_arithmetic_reduce_conformance(
op_cls: type,
torch_fn: Callable,
dim,
keepdim: bool,
dtype: torch.dtype,
) -> None:
"""Each (op, dim-shape, keepdim, dtype) cell must match PyTorch."""
torch.manual_seed(0)
x = torch.randn(*_SHAPE, dtype=dtype, device=DEVICE)
op = op_cls(dtype=dtype, dim=dim, keepdim=keepdim)
y = op(x)
ref = _ref(torch_fn, x, dim, keepdim)
assert y.shape == ref.shape, (
f"{op_cls.__name__} dim={dim} keepdim={keepdim} dtype={dtype}: "
f"shape {y.shape} vs ref {ref.shape}"
)
torch.testing.assert_close(y, ref, **_tol(dtype))
@pytest.mark.smoke
@pytest.mark.parametrize("op_cls, torch_fn", _OP_CASES, ids=[c[0].__name__ for c in _OP_CASES])
def test_dim_none_keepdim_false_returns_0d(op_cls: type, torch_fn: Callable) -> None:
"""``dim=None, keepdim=False`` must return a 0-D tensor matching PyTorch."""
x = torch.randn(*_SHAPE, dtype=torch.float32, device=DEVICE)
op = op_cls(dtype=torch.float32, dim=None, keepdim=False)
y = op(x)
ref = _ref(torch_fn, x, None, False)
assert y.ndim == 0, f"{op_cls.__name__}: expected 0-D, got shape {y.shape}"
assert ref.ndim == 0
torch.testing.assert_close(y, ref, atol=1e-4, rtol=1e-4)
@pytest.mark.smoke
@pytest.mark.parametrize(
"op_cls, torch_fn", _OP_CASES, ids=[c[0].__name__ for c in _OP_CASES],
)
@pytest.mark.parametrize(
"dim",
[
pytest.param(-1, id="dim=int"),
pytest.param((0, 2), id="dim=tuple"),
pytest.param(None, id="dim=None"),
],
)
def test_arithmetic_reduce_unaligned_innermost(
op_cls: type, torch_fn: Callable, dim,
) -> None:
"""Unaligned innermost dim must still match PyTorch.
The aligned ``_SHAPE`` (innermost = 256, a kernel-tile multiple) bypasses
the simple-reduce kernel's masked-load boundary path. Use 255 to flush
the pad branch on every (op, dim-mode) cell.
"""
torch.manual_seed(0)
unaligned_shape = (4, 8, 255)
dtype = torch.float16
x = torch.randn(*unaligned_shape, dtype=dtype, device=DEVICE)
op = op_cls(dtype=dtype, dim=dim, keepdim=False)
y = op(x)
ref = _ref(torch_fn, x, dim, False)
assert y.shape == ref.shape, (
f"{op_cls.__name__} dim={dim} unaligned: shape {y.shape} vs ref {ref.shape}"
)
torch.testing.assert_close(y, ref, **_tol(dtype))
if __name__ == "__main__":
pytest.main([__file__, "-vvs"])