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