Skip to content

Commit 8dc8ae0

Browse files
committed
move precision check logic into separate module
1 parent a75bc78 commit 8dc8ae0

2 files changed

Lines changed: 217 additions & 184 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Precision-checking register – loaded only when precision checking is enabled.
2+
3+
This module is NOT imported on the normal execution path. It is lazily
4+
imported by ``register.py`` only when the user explicitly requests
5+
``PrecisionCheckRegister``.
6+
"""
7+
8+
import functools
9+
10+
import torch
11+
12+
from ..logging_utils import (
13+
compare_outputs,
14+
get_precision_logger,
15+
get_tensor_info,
16+
precision_config,
17+
)
18+
from .register import Register
19+
20+
# Maximum tensor element count allowed for precision check
21+
# (skip if exceeded to avoid large tensor copy overhead)
22+
_MAX_NUMEL_FOR_CHECK = 1 * 1024 * 1024 # 1M elements
23+
24+
25+
def _get_dtype_tolerance(args, default_rtol, default_atol):
26+
"""Automatically adjust tolerance based on the dtype of input tensors."""
27+
for a in args:
28+
if isinstance(a, torch.Tensor) and a.is_floating_point():
29+
if a.dtype in (torch.bfloat16, torch.float16):
30+
return (max(default_rtol, 1e-2), max(default_atol, 1e-2))
31+
break
32+
return (default_rtol, default_atol)
33+
34+
35+
def _to_cpu(x):
36+
"""Recursively move tensors to CPU."""
37+
if isinstance(x, torch.Tensor):
38+
return x.detach().cpu()
39+
elif isinstance(x, (list, tuple)):
40+
return type(x)(_to_cpu(i) for i in x)
41+
elif isinstance(x, dict):
42+
return {k: _to_cpu(v) for k, v in x.items()}
43+
return x
44+
45+
46+
def _max_tensor_numel(args):
47+
"""Return the element count of the largest tensor in the arguments."""
48+
max_n = 0
49+
for a in args:
50+
if isinstance(a, torch.Tensor):
51+
max_n = max(max_n, a.numel())
52+
return max_n
53+
54+
55+
# Operators that should never be precision-checked
56+
_SKIP_OPS = frozenset(
57+
{
58+
# Pure layout / memory operations
59+
"copy_",
60+
"_to_copy",
61+
"view",
62+
"reshape",
63+
"expand",
64+
"permute",
65+
"transpose",
66+
"contiguous",
67+
"clone",
68+
"to",
69+
"empty",
70+
"zeros",
71+
"ones",
72+
"full",
73+
"masked_fill_",
74+
# Random sampling operators (GPU/CPU RNGs differ)
75+
"exponential_",
76+
"normal_",
77+
"uniform_",
78+
"bernoulli_",
79+
"random_",
80+
"multinomial",
81+
"randperm",
82+
# Sorting/selection operators (order of equal values may differ)
83+
"sort",
84+
"topk",
85+
"argsort",
86+
}
87+
)
88+
89+
90+
def _wrap_op_with_precision_check(op_key, fn):
91+
"""Wrap a FlagGems operator to compare its output against native PyTorch.
92+
93+
Since FlagGems replaces the CUDA dispatch, the native implementation
94+
cannot be called on GPU, so inputs are copied to CPU to compute the
95+
reference result. Performance overhead is controlled by:
96+
- max_checks: only check the first N calls per operator (default 10)
97+
- skip large tensors (over 1M elements)
98+
- once a failure is logged, that operator is no longer checked
99+
"""
100+
_call_count = 0
101+
102+
@functools.wraps(fn)
103+
def wrapper(*args, **kwargs):
104+
nonlocal _call_count
105+
106+
# Execute the FlagGems implementation first
107+
fg_result = fn(*args, **kwargs)
108+
109+
cfg = precision_config
110+
111+
# Skip operators that have already logged a failure
112+
if op_key in cfg["logged_ops"]:
113+
return fg_result
114+
115+
# Sampling: only check the first N calls per operator
116+
_call_count += 1
117+
if _call_count > cfg.get("max_checks", 10):
118+
return fg_result
119+
120+
op_name = (
121+
op_key.split("::")[-1].split(".")[0]
122+
if "::" in op_key
123+
else op_key.split(".")[0]
124+
)
125+
126+
# Skip out variants
127+
overload_part = op_key.split(".")[-1] if "." in op_key else ""
128+
if overload_part == "out" or op_name.endswith("_out"):
129+
return fg_result
130+
131+
# Skip operators that do not need checking
132+
if op_name in _SKIP_OPS:
133+
return fg_result
134+
135+
# Skip large tensors to avoid copy overhead
136+
if _max_tensor_numel(args) > _MAX_NUMEL_FOR_CHECK:
137+
return fg_result
138+
139+
try:
140+
parts = op_key.split(".")
141+
base_name = parts[0]
142+
overload_name = parts[1] if len(parts) > 1 else "default"
143+
144+
aten_packet = getattr(torch.ops.aten, base_name, None)
145+
if aten_packet is None:
146+
return fg_result
147+
aten_overload = getattr(aten_packet, overload_name, None)
148+
if aten_overload is None:
149+
return fg_result
150+
151+
cpu_args = [_to_cpu(a) for a in args]
152+
cpu_kwargs = {k: _to_cpu(v) for k, v in kwargs.items()}
153+
154+
with torch.no_grad():
155+
pt_result_cpu = aten_overload(*cpu_args, **cpu_kwargs)
156+
157+
fg_result_cpu = _to_cpu(fg_result)
158+
159+
rtol, atol = _get_dtype_tolerance(args, cfg["rtol"], cfg["atol"])
160+
is_close, info = compare_outputs(fg_result_cpu, pt_result_cpu, rtol, atol)
161+
162+
if not is_close:
163+
cfg["logged_ops"].add(op_key)
164+
logger = get_precision_logger()
165+
input_info = [
166+
get_tensor_info(a) for a in args if get_tensor_info(a)
167+
]
168+
output_info = get_tensor_info(fg_result)
169+
170+
msg = (
171+
f"Op: {op_key} | FAIL | in: {input_info} | out: {output_info}"
172+
)
173+
if "error" in info:
174+
msg += (
175+
f" | {info['error']}: fg={info['fg']}, pt={info['pt']}"
176+
)
177+
else:
178+
msg += (
179+
f" | max_abs: {info['max_abs']:.6e}"
180+
f" | max_rel: {info['max_rel']:.6e}"
181+
)
182+
msg += f" | rtol={rtol}, atol={atol}"
183+
logger.warning(msg)
184+
185+
except Exception:
186+
pass
187+
188+
return fg_result
189+
190+
return wrapper
191+
192+
193+
class PrecisionCheckRegister(Register):
194+
"""Register subclass that wraps every operator with precision checking.
195+
196+
This class is only instantiated when the user has explicitly called
197+
``enable_precision_check()`` before ``enable()`` / ``only_enable()``.
198+
It is never on the normal execution path.
199+
"""
200+
201+
def register_impl(self, key, fn):
202+
if self.lib is None:
203+
raise ValueError("Library instance is not provided.")
204+
205+
wrapped_fn = _wrap_op_with_precision_check(key, fn)
206+
207+
device_key = self.reg_key
208+
self.all_ops.append(fn.__name__)
209+
self.all_keys.append(key)
210+
self.lib.impl(key, wrapped_fn, device_key)

0 commit comments

Comments
 (0)