Skip to content

Commit 4663a9a

Browse files
committed
add precison check
1 parent e802f80 commit 4663a9a

2 files changed

Lines changed: 272 additions & 16 deletions

File tree

src/flag_gems/logging_utils.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
"""
1212

1313
import logging
14+
import functools
15+
import traceback
1416
from pathlib import Path
1517

18+
import torch
1619

1720
class LogOncePerLocationFilter(logging.Filter):
1821
def __init__(self):
@@ -68,3 +71,93 @@ def teardown_flaggems_logging(logger: logging.Logger | None = None):
6871

6972
logger = logger or logging.getLogger("flag_gems")
7073
_remove_file_handlers(logger)
74+
75+
76+
# prevision check logger
77+
78+
_precision_logger = None
79+
precision_config = {
80+
"enabled": False,
81+
"rtol": 1e-4,
82+
"atol": 1e-5,
83+
"log_once": True,
84+
"logged_ops": set(),
85+
}
86+
87+
88+
def setup_precision_logging(path=None):
89+
global _precision_logger
90+
_precision_logger = logging.getLogger("flag_gems.precision")
91+
_remove_file_handlers(_precision_logger)
92+
93+
filename = Path(path or Path.home() / ".flaggems/precision.log")
94+
filename.parent.mkdir(parents=True, exist_ok=True)
95+
handler = logging.FileHandler(filename, mode="w")
96+
handler._flaggems_owned = True
97+
98+
formatter = logging.Formatter(
99+
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
100+
)
101+
handler.setFormatter(formatter)
102+
_precision_logger.setLevel(logging.DEBUG)
103+
_precision_logger.addHandler(handler)
104+
_precision_logger.propagate = False
105+
106+
107+
def get_precision_logger():
108+
return _precision_logger
109+
110+
111+
def get_tensor_info(t):
112+
if isinstance(t, torch.Tensor):
113+
return f"{tuple(t.shape)}:{t.dtype}"
114+
elif isinstance(t, (list, tuple)):
115+
infos = [get_tensor_info(x) for x in t]
116+
return [i for i in infos if i]
117+
return None
118+
119+
120+
def get_call_location():
121+
for frame in traceback.extract_stack():
122+
if "flag_gems" not in frame.filename and "torch" not in frame.filename:
123+
return f"{frame.filename}:{frame.lineno}"
124+
return "unknown"
125+
126+
127+
def compare_outputs(fg_out, pt_out, rtol, atol):
128+
if isinstance(fg_out, torch.Tensor) and isinstance(pt_out, torch.Tensor):
129+
if fg_out.shape != pt_out.shape:
130+
return False, {"error": "shape_mismatch", "fg": tuple(fg_out.shape), "pt": tuple(pt_out.shape)}
131+
try:
132+
fg = fg_out.detach().float()
133+
pt = pt_out.detach().float()
134+
abs_diff = torch.abs(fg - pt)
135+
max_abs = abs_diff.max().item()
136+
max_rel = (abs_diff / (torch.abs(pt) + 1e-12)).max().item()
137+
is_close = torch.allclose(fg, pt, rtol=rtol, atol=atol)
138+
return is_close, {"max_abs": max_abs, "max_rel": max_rel}
139+
except:
140+
return True, {}
141+
elif isinstance(fg_out, (tuple, list)) and isinstance(pt_out, (tuple, list)):
142+
for i, (fg, pt) in enumerate(zip(fg_out, pt_out)):
143+
ok, info = compare_outputs(fg, pt, rtol, atol)
144+
if not ok:
145+
info["index"] = i
146+
return False, info
147+
return True, {}
148+
149+
def enable_precision_check(rtol=1e-4, atol=1e-5, log_once=True, max_checks=10, path=None):
150+
setup_precision_logging(path)
151+
precision_config.update({
152+
"enabled": True,
153+
"rtol": rtol,
154+
"atol": atol,
155+
"log_once": log_once,
156+
"max_checks": max_checks,
157+
"logged_ops": set(),
158+
})
159+
160+
161+
def disable_precision_check():
162+
"""close precision log file and disable precision check."""
163+
precision_config["enabled"] = False

src/flag_gems/runtime/register.py

Lines changed: 179 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import warnings
2+
import functools
3+
import torch
24

35
from . import backend, common, error
46
from .backend.device import DeviceDetector
7+
from ..logging_utils import (
8+
precision_config,
9+
get_precision_logger,
10+
get_tensor_info,
11+
get_call_location,
12+
compare_outputs,
13+
)
514

615

716
class Register:
@@ -24,10 +33,7 @@ def __init__(
2433
self.reg_key = self.device.dispatch_key
2534
self.all_ops = []
2635
self.all_keys = []
27-
if self.device.vendor == common.vendors.CAMBRICON:
28-
# TODO: Cambricon specific, to avoid op deadlock question in libtuner.
29-
# Should remove this in the future.
30-
self.torch_ops_map = {}
36+
3137

3238
# optional mapping func_name -> list of config entries
3339
self.full_config_by_func = full_config_by_func
@@ -114,18 +120,7 @@ def register_impl(self, key, fn):
114120
device_key = self.reg_key
115121
self.all_ops.append(fn.__name__)
116122
self.all_keys.append(key)
117-
if self.device.vendor == common.vendors.CAMBRICON:
118-
import torch
119-
120-
try:
121-
self.torch_ops_map["aten::" + key] = torch.library.get_kernel(
122-
"aten::" + key, device_key
123-
)
124-
except Exception:
125-
pass
126-
self.lib.impl(key, fn, device_key, allow_override=True)
127-
else:
128-
self.lib.impl(key, fn, device_key)
123+
self.lib.impl(key, fn, device_key)
129124

130125
def for_each(self):
131126
for key, func in self.config:
@@ -148,3 +143,171 @@ def get_vendor_name(self):
148143

149144
def get_current_device(self):
150145
return self.device.name
146+
147+
# def _wrap_op_with_precision_check(op_key, fn):
148+
# @functools.wraps(fn)
149+
# def wrapper(*args, **kwargs):
150+
# fg_result = fn(*args, **kwargs)
151+
152+
# cfg = precision_config
153+
# if cfg["enabled"] and op_key not in cfg["logged_ops"]:
154+
# cfg["logged_ops"].add(op_key)
155+
# logger = get_precision_logger()
156+
# input_info = [get_tensor_info(a) for a in args if get_tensor_info(a)]
157+
# logger.info(f"Op: {op_key} | in: {input_info}")
158+
159+
# return fg_result
160+
# return wrapper
161+
162+
# 精度检查时允许的最大 tensor 元素数(超过则跳过,避免大 tensor 拷贝开销)
163+
_MAX_NUMEL_FOR_CHECK = 1 * 1024 * 1024 # 1M 元素
164+
165+
166+
def _get_dtype_tolerance(args, default_rtol, default_atol):
167+
"""根据输入 tensor 的 dtype 自动调整容差"""
168+
for a in args:
169+
if isinstance(a, torch.Tensor) and a.is_floating_point():
170+
if a.dtype in (torch.bfloat16, torch.float16):
171+
return (max(default_rtol, 1e-2), max(default_atol, 1e-2))
172+
break
173+
return (default_rtol, default_atol)
174+
175+
176+
def _to_cpu(x):
177+
"""递归地将 tensor 移到 CPU"""
178+
if isinstance(x, torch.Tensor):
179+
return x.detach().cpu()
180+
elif isinstance(x, (list, tuple)):
181+
return type(x)(_to_cpu(i) for i in x)
182+
elif isinstance(x, dict):
183+
return {k: _to_cpu(v) for k, v in x.items()}
184+
return x
185+
186+
187+
def _max_tensor_numel(args):
188+
"""返回参数中最大 tensor 的元素数"""
189+
max_n = 0
190+
for a in args:
191+
if isinstance(a, torch.Tensor):
192+
max_n = max(max_n, a.numel())
193+
return max_n
194+
195+
196+
def _wrap_op_with_precision_check(op_key, fn):
197+
"""包装 FlagGems 算子,在执行后与 PyTorch 原生 CPU 实现做精度对比。
198+
199+
由于 FlagGems 替换了 CUDA dispatch,无法在 GPU 上调用原生实现,
200+
因此将输入拷贝到 CPU 计算参考结果。通过以下方式控制性能开销:
201+
- max_checks: 每个算子只检查前 N 次调用(默认 10)
202+
- 跳过大 tensor(超过 1M 元素)
203+
- 一旦记录过失败就不再检查该算子
204+
"""
205+
_call_count = 0
206+
207+
@functools.wraps(fn)
208+
def wrapper(*args, **kwargs):
209+
nonlocal _call_count
210+
211+
# 先执行 FlagGems 实现
212+
fg_result = fn(*args, **kwargs)
213+
214+
cfg = precision_config
215+
if not cfg["enabled"]:
216+
return fg_result
217+
218+
# 已经记录过失败的算子不再检查
219+
if op_key in cfg["logged_ops"]:
220+
return fg_result
221+
222+
# 采样:每个算子只检查前 N 次调用
223+
_call_count += 1
224+
if _call_count > cfg.get("max_checks", 10):
225+
return fg_result
226+
227+
op_name = op_key.split("::")[-1].split(".")[0] if "::" in op_key else op_key.split(".")[0]
228+
229+
# 跳过 out variant(参数签名不同,CPU 调用容易出错)
230+
overload_part = op_key.split(".")[-1] if "." in op_key else ""
231+
if overload_part == "out" or op_name.endswith("_out"):
232+
return fg_result
233+
234+
# 跳过不需要检查的算子
235+
skip_ops = {
236+
# 纯 layout / 内存操作
237+
'copy_', '_to_copy', 'view', 'reshape', 'expand', 'permute',
238+
'transpose', 'contiguous', 'clone', 'to', 'empty', 'zeros',
239+
'ones', 'full', 'masked_fill_',
240+
# 随机采样算子(GPU/CPU 随机数生成器不同,结果必然不一致)
241+
'exponential_', 'normal_', 'uniform_', 'bernoulli_', 'random_',
242+
'multinomial', 'randperm',
243+
# 排序/选择算子(相同值的排序顺序可能不同,不适合逐元素比较)
244+
'sort', 'topk', 'argsort',
245+
}
246+
if op_name in skip_ops:
247+
return fg_result
248+
249+
# 跳过大 tensor,避免拷贝开销
250+
if _max_tensor_numel(args) > _MAX_NUMEL_FOR_CHECK:
251+
return fg_result
252+
253+
try:
254+
# 解析 op_key 获取正确的 overload
255+
# op_key 形如 "add.Tensor", "mm.default", "softmax.int" 等
256+
parts = op_key.split(".")
257+
base_name = parts[0]
258+
overload_name = parts[1] if len(parts) > 1 else "default"
259+
260+
aten_packet = getattr(torch.ops.aten, base_name, None)
261+
if aten_packet is None:
262+
return fg_result
263+
aten_overload = getattr(aten_packet, overload_name, None)
264+
if aten_overload is None:
265+
return fg_result
266+
267+
# 将输入拷贝到 CPU,调用原生 aten 实现(CPU 上不受 FlagGems 影响)
268+
cpu_args = [_to_cpu(a) for a in args]
269+
cpu_kwargs = {k: _to_cpu(v) for k, v in kwargs.items()}
270+
271+
with torch.no_grad():
272+
pt_result_cpu = aten_overload(*cpu_args, **cpu_kwargs)
273+
274+
# 将 FlagGems 结果也拷贝到 CPU 做比较(避免把 CPU 结果搬回 GPU)
275+
fg_result_cpu = _to_cpu(fg_result)
276+
277+
# 根据 dtype 自动调整容差
278+
rtol, atol = _get_dtype_tolerance(args, cfg["rtol"], cfg["atol"])
279+
280+
is_close, info = compare_outputs(fg_result_cpu, pt_result_cpu, rtol, atol)
281+
282+
if not is_close:
283+
cfg["logged_ops"].add(op_key)
284+
logger = get_precision_logger()
285+
input_info = [get_tensor_info(a) for a in args if get_tensor_info(a)]
286+
output_info = get_tensor_info(fg_result)
287+
288+
msg = f"Op: {op_key} | FAIL | in: {input_info} | out: {output_info}"
289+
if "error" in info:
290+
msg += f" | {info['error']}: fg={info['fg']}, pt={info['pt']}"
291+
else:
292+
msg += f" | max_abs: {info['max_abs']:.6e} | max_rel: {info['max_rel']:.6e}"
293+
msg += f" | rtol={rtol}, atol={atol}"
294+
logger.warning(msg)
295+
296+
except Exception:
297+
pass
298+
299+
return fg_result
300+
301+
return wrapper
302+
303+
class PrecisionCheckRegister(Register):
304+
def register_impl(self, key, fn):
305+
if self.lib is None:
306+
raise ValueError("Library instance is not provided.")
307+
308+
wrapped_fn = _wrap_op_with_precision_check(key, fn)
309+
310+
device_key = self.reg_key
311+
self.all_ops.append(fn.__name__)
312+
self.all_keys.append(key)
313+
self.lib.impl(key, wrapped_fn, device_key)

0 commit comments

Comments
 (0)