diff --git a/agents/triton-ascend-coder.md b/agents/triton-ascend-coder.md index 8fa12a61..a8189607 100644 --- a/agents/triton-ascend-coder.md +++ b/agents/triton-ascend-coder.md @@ -152,7 +152,7 @@ Agent 自身维护迭代状态,编排 "生成 → 验证 → Conductor 分析" ``` iteration = 0 -max_iterations = 5 +max_iterations = 20 history_attempts = [] previous_code = "" verifier_error = "" diff --git a/skills/triton/kernel-verifier/SKILL.md b/skills/triton/kernel-verifier/SKILL.md index e1cf7552..6a4a6241 100644 --- a/skills/triton/kernel-verifier/SKILL.md +++ b/skills/triton/kernel-verifier/SKILL.md @@ -16,6 +16,108 @@ argument-hint: > 你是一个内核代码验证专家。你的任务是按照标准验证流程,创建验证项目并运行,检查生成的算子代码是否能正确编译运行且与参考实现的输出一致。验证通过后,执行性能测试并收集性能数据。 +## 精度判定规则 + +> 本节是 verify.py 精度判定的**唯一权威说明**。所有阈值、决策矩阵、前置检查只在此处定义;下游章节(Step 2/3)只引用、不重复。 +> (别名:精度阈值说明 / 验证分类与判定标准) + +verify.py 按"`--non-compute` 开关 + 输入 dtype + 输出 dtype"分流到 **5 类判定路径**。 + +### 1. 输入类型推断(KernelBench / NPUKernelBench 统一) + +从实际传入对象推断(不依赖 task 文件结构化 spec): + +1. 存在 `torch.Tensor` 输入 → 取所有 tensor 中**最高精度 dtype** + (例:输入为 `[fp16 tensor, fp32 tensor, int64 tensor]` → 取 fp32) +2. 否则存在 `list/tuple of Tensor`(tensor_list)→ 取首个 tensor_list 首元素 dtype +3. 否则视为**无 tensor 输入**(`no_tensor`,最严路径) + +**dtype 优先级**: + +精度从高到低排列:float64 > float32 > float16 > bfloat16 > float8_e4m3 / float8_e5m2 > int64 > int32 > int16 > int8 / uint8 > bool + +**`input_type` 二分类**(用于 §2 决策矩阵分流): + +- `input_type = float`:输入最高精度 dtype 属于浮点族(float64/32/16、bfloat16、float8_e4m3/e5m2、复数 complex64/128) +- `input_type = int`:输入最高精度 dtype 属于整型族(int64/32/16/8、uint8、bool) +- `input_type = no_tensor`:无任何 tensor 输入(走最严判定) + +> **关于 bool 的两处特殊性**(避免混淆): +> - bool 作**输入**:归入 `int`,与 int 系输入等同;分流时只看输出 dtype(如输出 fp 走浮点判定) +> - bool 作**输出**:不进入 input_type 分流,直接走 §2 "bool 输出"路径(`torch.equal` 严格相等) + +### 2. 五类判定决策矩阵 + +| 类别 | 输入 type | 输出 dtype | `--non-compute` | 误差要求 | +|---|---|---|---|---| +| **非计算类** | 任意 | 任意 | **是** | 二进制完全一致(view-as-int 比对,含 NaN bit pattern) | +| **bool 输出** | 任意 | bool | 否 | `torch.equal` 严格相等 | +| **整数计算类** | int / no_tensor | int | 否 | `\|actual − golden\| == 0` | +| **量化计算类 fp→int** | float | int | 否 | `\|actual − golden\| <= 1` | +| **浮点计算类** | 任意 | float | 否 | 三项 AND(见 §4) | + +### 3. 比对前置检查(按顺序,任一失败即判 fail) + +1. 形状必须一致 +2. NaN 位置必须完全一致(mask 按位相等) +3. Inf 位置和符号必须完全一致 +4. `bool` dtype:要求 `torch.equal` 完全相等,不进入精度判定 +5. 仅在 `finite_mask`(双方都 finite)上做精度计算;dtype 不一致时 impl 会被 cast 到 golden 的 dtype + +### 4. 浮点计算类:三项 AND 整体判定 + +#### 4.1 元素级 matched 定义(分桶) + +对每个 finite 元素 `i`,按 `|golden[i]|` 落入的类别分别判定: + +- **小值域** `|golden[i]| < small_value_threshold`: + `matched[i] = (|actual[i] - golden[i]| <= small_value_error)` +- **正常域** `|golden[i]| >= small_value_threshold`: + `matched[i] = (|actual[i] - golden[i]| / (|golden[i]| + 1e-7) <= rel_threshold)` + +> 计算前两侧统一升 float32,避免低精度 dtype 自身误差污染。 +> 分母 `+1e-7` 仅为保险——正常域里 `|golden| >= sv_thr ≫ 1e-7`。 + +#### 4.2 三项通过条件(AND,全部满足才算通过) + +1. **`max_error_cap`**:所有 finite 元素满足 `|diff| <= atol + rtol * |golden|`(dtype-aware,要求 100% 通过) +2. **`required_matched_ratio`**:`sum(matched) / total_finite >= 0.9` +3. **`MERE`**:对所有 finite 元素计算 `rel_err = |diff| / (|golden| + 1e-7)` 再取均值,要求 `MERE < rel_threshold`。当 `total_finite == 0` 时本项自动通过。 + +#### 4.3 阈值表 + +**matched_mask 与 MERE 阈值**(沿用 NPU Benchmark 标准): + +| 数据类型 | small_value_threshold | small_value_error | rel_threshold (= MERE 上限) | +|---|---|---|---| +| `float16` | 2⁻¹¹ ≈ 4.88e-4 | 2⁻¹⁶ ≈ 1.53e-5 | 2⁻¹⁰ ≈ 9.77e-4 | +| `bfloat16` | 2⁻⁸ ≈ 3.91e-3 | 2⁻¹⁶ ≈ 1.53e-5 | 2⁻⁷ ≈ 7.81e-3 | +| `float32` | 2⁻¹⁴ ≈ 6.10e-5 | 2⁻³⁰ ≈ 9.31e-10 | 2⁻¹³ ≈ 1.22e-4 | +| `hifloat32` | 2⁻¹² ≈ 2.44e-4 | 2⁻²⁸ ≈ 3.73e-9 | 2⁻¹¹ ≈ 4.88e-4 | +| `float8_e4m3` | 2⁻⁴ = 0.0625 | 2⁻⁶ ≈ 0.015625 | 2⁻³ = 0.125 | +| `float8_e5m2` | 2⁻³ = 0.125 | 2⁻⁵ = 0.03125 | 2⁻² = 0.25 | +| 其他 dtype(fallback) | 2⁻¹⁴ | 2⁻³⁰ | 2⁻¹³ | + +**max_error_cap 阈值**(`|diff| <= atol + rtol * |golden|`): + +| 数据类型 | atol | rtol | +|---|---|---| +| `float16` | 9e-2 | 2⁻¹⁰ ≈ 9.77e-4 | +| `bfloat16` | 1e-1 | 2⁻⁷ ≈ 7.81e-3 | +| `float32` | 1e-3 | 2⁻¹³ ≈ 1.22e-4 | +| 其他 dtype(fallback) | 1e-3 | 2⁻¹³ | + +### 5. 运行时诊断输出 + +verify.py 在每个 case 会向 stderr 打印: + +- `[输入类型判定] 来源=...,候选 dtypes=...,最高精度=...,input_type=...` +- `[评测模式] 模式=...,输入 dtype=...,输出 dtype=...,误差要求=...` + +便于上游 agent 立即看到当前 case 落入了哪一类、用了哪些阈值。 + +--- + ## 验证流程 ``` @@ -123,14 +225,18 @@ python3 /path/to/kernel-verifier/scripts/verify.py \ | `--op_name` | 是 | 算子名称,与文件名前缀对应 | | `--verify_dir` | 否 | 验证目录路径,默认当前目录 | | `--triton_impl_name` | 否 | Triton 实现模块名(不含 `{op_name}_` 前缀),默认 `triton_ascend_impl` | -| `--timeout` | 否 | 超时秒数,默认 900 | +| `--timeout` | 否 | 超时秒数,默认 900(⚠️ 当前已忽略:脚本为同进程串行模式,未实现超时强制中止) | +| `--output` | 否 | 验证结果 JSON 输出路径,默认 `{verify_dir}/verify_result.json` | +| `--non-compute` | 否 | 适用于非计算类算子(不做数值运算、只对张量进行形状变换、维度重排、切分拼接、索引、类型转换等数据重组操作的算子,常见如 Reshape、Transpose、Concat、Split、Gather、Cast、Pad 等),强制走二进制完全一致判定 | -**超时设置**:默认 900 秒,复杂算子可适当增加。 +**超时设置**:`--timeout` 参数当前已忽略(同进程串行模式),保留仅为兼容旧调用方。 -**⛔ 禁止事项**: +**注意事项**: - 禁止自己编写 Python 代码来测试算子(如手动 import 并 forward 比较) - 禁止使用 `torch.allclose` 或其他自创方法替代 `scripts/verify.py` - 禁止跳过此步骤直接报告验证结果 +- 禁止对计算类算子(含数值运算)传 `--non-compute`;该开关仅适用于不做数值运算、只做形状变换 / 维度重排 / 切分拼接 / 索引 / 类型转换等数据重组操作的算子(如 Reshape、Transpose、Concat、Split、Gather、Cast、Pad)。误用会强制走二进制完全一致判定,把正常的浮点舍入差异判为失败 +- 对非计算类算子(例如形状变换、维度重排、切分拼接、索引、类型转换等数据重组操作的算子)**一定要**传 `--non-compute`;漏传会让此类算子按浮点三项判定走,容许超出预期的差异,无法识别真正的位级不一致 --- @@ -157,13 +263,49 @@ verify.py 会在 `verify_dir` 下生成 `verify_result.json`(或 `--output` } ``` +**精度失败时的 `metrics` 字段**:当 `error_type == "AccuracyError"`(浮点三项判定未通过)时,`failures[*]` 会带上结构化 `metrics`,便于下游分类失败原因(max_error_cap 违例 / 离群点过多 / 平均误差偏大): + +```json +{ + "case_idx": 1, + "input_desc": [...], + "error_type": "AccuracyError", + "error_msg": "...", + "metrics": { + "matched_ratio": 0.95, + "max_abs_diff": 0.2, + "MERE": 2.0e-4, + "rel_threshold": 1.22e-4, + "small_value_threshold": 6.10e-5, + "small_value_error": 9.31e-10, + "atol": 1.0e-3, + "rtol": 1.22e-4, + "max_error_cap_violation_count": 12, + "required_matched_ratio": 0.9, + "total_finite": 1000, + "matched_count": 950, + "small_count": 0, + "normal_count": 1000, + "checks": { + "max_error_cap": false, + "required_matched_ratio": false, + "MERE": false + } + } +} +``` + +`checks` 三个布尔位标记每项判定是否独立通过。阈值定义见上文 §精度判定规则 §4。 + +非浮点类失败(`non_compute` / `bool_output` / `integer_compute` / `quant_fp_to_int`)的 `metrics` 字段较简单,含 `category` / `violation_count` / `total_*` 等基本计数。 + **多 shape 行为**:每个 shape 独立 try/except,失败不中止后续 shape;全部跑完才落盘并退出。 **退出码语义(策略 A:严格)**: -- `passed_cases == total_cases` → exit 0,`verifier_result = true` -- `passed_cases < total_cases` → exit 1,`verifier_result = false`,`verifier_error` 应读取 `verify_result.json.failures` 的**全部条目**(不是第一个),汇总后提交给 Conductor。 +- `passed_cases == total_cases` 且 `total_cases > 0` → exit 0,`verifier_result = true` +- 否则(`passed_cases < total_cases`,或 `total_cases == 0`)→ exit 1,`verifier_result = false`,`verifier_error` 应读取 `verify_result.json.failures` 的**全部条目**(不是第一个),汇总后提交给 Conductor。 -**超时**:脚本输出 `"验证超时"` 且退出码为 1 → `verifier_error = "验证超时({timeout}秒)"`。 +**超时**:当前 `--timeout` 已被忽略,脚本不会主动中止;不会输出"验证超时"。 --- @@ -325,43 +467,6 @@ benchmark.py 启动时按 `--triton_impl_name` 推导对应的 verify_result 文 --- -## 精度阈值说明 - -验证使用基于数据类型的 **MERE/MARE 双门限相对误差**判定(NPU Benchmark 标准),与 `torch.allclose` 不同。 - -**判定公式**(必须同时满足): - -``` -MERE < threshold 且 MARE < 10 × threshold -``` - -其中: -- `MERE` = mean(|actual - golden| / max(|golden|, threshold)),平均相对误差 -- `MARE` = max(|actual - golden| / max(|golden|, threshold)),最大相对误差 -- 计算前两侧统一升 float32,避免低精度 dtype 自身误差污染 -- 分母用 `clamp(min=threshold)` 而非 `+epsilon`:当 `|golden| < threshold`(参考值已小到 dtype 精度极限)时,rel_err 退化为 `|diff| / threshold`,等价于按绝对误差归一化,避免零值/极小值附近误报 - -**dtype 阈值表**(2 的幂次方): - -| 数据类型 | threshold | MERE 上限 | MARE 上限 (10×t) | -|---------|-----------|-----------|------------------| -| `float16` | 2⁻¹⁰ ≈ 9.77e-4 | 9.77e-4 | 9.77e-3 | -| `bfloat16` | 2⁻⁷ ≈ 7.81e-3 | 7.81e-3 | 7.81e-2 | -| `float32` | 2⁻¹³ ≈ 1.22e-4 | 1.22e-4 | 1.22e-3 | -| `hifloat32` | 2⁻¹¹ ≈ 4.88e-4 | 4.88e-4 | 4.88e-3 | -| `float8_e4m3` | 2⁻³ = 0.125 | 0.125 | 1.25 | -| `float8_e5m2` | 2⁻² = 0.25 | 0.25 | 2.5 | -| 其他 dtype(fallback) | 2⁻¹³ | 1.22e-4 | 1.22e-3 | - -**比对前置检查**(按顺序,任一失败即判 fail): -1. 形状必须一致 -2. NaN 位置必须完全一致(mask 按位相等) -3. Inf 位置和符号必须完全一致 -4. `bool` dtype:要求 `torch.equal` 完全相等,不进入 MERE/MARE 判定 -5. 仅在 `finite_mask` 上做 MERE/MARE 计算;当 dtype 不一致时 impl 会被 cast 到 golden 的 dtype - ---- - ## 脚本位置 验证脚本位于本 skill 的 `scripts/` 目录: @@ -374,5 +479,5 @@ MERE < threshold 且 MARE < 10 × threshold **CLI 参数**: - `validate_triton_impl.py`: ``, `[--json]` -- `verify.py`: `--op_name`, `--verify_dir`, `--triton_impl_name`, `--timeout`, `--output` +- `verify.py`: `--op_name`, `--verify_dir`, `--triton_impl_name`, `--timeout`, `--output`, `--non-compute` - `benchmark.py`: `--op_name`, `--verify_dir`, `--triton_impl_name`, `--warmup`, `--repeats`, `--output`, `--skip_framework`, `--framework_latency_ms`, `--verify_not_required` \ No newline at end of file diff --git a/skills/triton/kernel-verifier/scripts/verify.py b/skills/triton/kernel-verifier/scripts/verify.py index 10ca3120..f83dfee6 100644 --- a/skills/triton/kernel-verifier/scripts/verify.py +++ b/skills/triton/kernel-verifier/scripts/verify.py @@ -4,23 +4,6 @@ 多 shape 模式下:每个 shape 独立 try/except,全部跑完后落盘 verify_result.json。 策略 A:passed < total 即整体判失败(exit 1),同时失败清单记录在 JSON 的 `failures` 字段。 -精度判定标准: - allclose 样式逐元素判定: - abs(actual - golden) <= atol + rtol * abs(golden) - -当前阈值: - FLOAT32: - rtol = 1.220703125e-4 | 2**(-13) - atol = 1e-5 - - FLOAT16: - rtol = 9.765625e-4 | 2**(-10) - atol = 1e-3 - - BFLOAT16: - rtol = 7.8125e-3 | 2**(-7) - atol = 1e-2 - 用法: python verify.py --op_name <算子名> [--verify_dir <验证目录>] [--timeout <超时秒数>] """ @@ -29,12 +12,29 @@ import json import os import sys -import subprocess import traceback ERROR_MSG_LIMIT = 2000 +REQUIRED_MATCHED_RATIO = 0.9 + +# allclose 判定阈值 (atol, rtol):|actual - golden| <= atol + rtol * |golden| +ALLCLOSE_TOLS_STR = { + "float32": (1e-3, 2**(-13)), # 2**(-13)=1.220703125e-4 + "float16": (9e-2, 2**(-10)), # 2**(-10)=9.765625e-4 + "bfloat16": (1e-1, 2**(-7)), # 2**(-7)=7.8125e-3 +} +ALLCLOSE_DEFAULT_TOLS = ALLCLOSE_TOLS_STR["float32"] + + +class AccuracyError(AssertionError): + """精度判定失败异常,附带结构化 metrics 便于下游统计。""" + + def __init__(self, message, metrics): + super().__init__(message) + self.metrics = metrics + def truncate_error(msg: str, limit: int = ERROR_MSG_LIMIT) -> str: if msg is None: @@ -51,7 +51,6 @@ def describe_input(inputs): import torch except Exception: torch = None - descs = [] for x in inputs: if torch is not None and isinstance(x, torch.Tensor): @@ -79,74 +78,182 @@ def cleanup_npu_memory(): gc.collect() -def get_allclose_tolerance(data_type): - """根据数据类型获取 allclose 样式精度阈值。 +def get_limits(data_type): + """根据数据类型返回精度判定的三元组 (small_value_threshold, small_value_error, rel_threshold)。 + + 参考 NPU Benchmark 精度对比方法: + - small_value_threshold:判定元素是否落在"小值域"的阈值 + - small_value_error:小值域元素的绝对误差上限 + - rel_threshold:正常值域元素的相对误差上限,同时也是 MERE 的判定阈值 + + 阈值表: + | 数据类型 | small_value_threshold | small_value_error | rel_threshold | + |--------------|-----------------------|-------------------|---------------| + | FLOAT16 | 2^{-11} | 2^{-16} | 2^{-10} | + | BFLOAT16 | 2^{-8} | 2^{-16} | 2^{-7} | + | FLOAT32 | 2^{-14} | 2^{-30} | 2^{-13} | + | HiFloat32 | 2^{-12} | 2^{-28} | 2^{-11} | + | FLOAT8 E4M3 | 2^{-4} | 2^{-6} | 2^{-3} | + | FLOAT8 E5M2 | 2^{-3} | 2^{-5} | 2^{-2} | + + 由于 torch.dtype 中没有直接定义 HiFloat32,可通过字符串传入 "hifloat32" 获取对应阈值。 + """ # noqa: E501 + import torch + + # 字符串映射(用于 HiFloat32 或其他自定义类型) + str_to_limits = { + "float16": (2**(-11), 2**(-16), 2**(-10)), + "bfloat16": (2**(-8), 2**(-16), 2**(-7)), + "float32": (2**(-14), 2**(-30), 2**(-13)), + "hifloat32": (2**(-12), 2**(-28), 2**(-11)), + "float8_e4m3": (2**(-4), 2**(-6), 2**(-3)), + "float8_e5m2": (2**(-3), 2**(-5), 2**(-2)), + "fp8_e4m3": (2**(-4), 2**(-6), 2**(-3)), + "fp8_e5m2": (2**(-3), 2**(-5), 2**(-2)), + } + if isinstance(data_type, str): + return str_to_limits.get(data_type.lower(), (2**(-14), 2**(-30), 2**(-13))) + + # torch.dtype 映射 + dtype_limits_map = { + torch.float16: (2**(-11), 2**(-16), 2**(-10)), + torch.bfloat16: (2**(-8), 2**(-16), 2**(-7)), + torch.float32: (2**(-14), 2**(-30), 2**(-13)), + } + + float8_e4m3 = getattr(torch, 'float8_e4m3fn', None) or getattr(torch, 'float8_e4m3', None) + if float8_e4m3 is not None: + dtype_limits_map[float8_e4m3] = (2**(-4), 2**(-6), 2**(-3)) + + float8_e5m2 = getattr(torch, 'float8_e5m2fn', None) or getattr(torch, 'float8_e5m2', None) + if float8_e5m2 is not None: + dtype_limits_map[float8_e5m2] = (2**(-3), 2**(-5), 2**(-2)) - 判定标准: - abs(actual - golden) <= atol + rtol * abs(golden) + return dtype_limits_map.get(data_type, (2**(-14), 2**(-30), 2**(-13))) - 当前采用阈值: - FLOAT32: - rtol = 2^{-13} = 1.220703125e-4 - atol = 1e-5 - FLOAT16: - rtol = 2^{-10} = 9.765625e-4 - atol = 1e-3 +def get_allclose_tols(data_type): + """根据数据类型返回 allclose 判定的 (atol, rtol)。 - BFLOAT16: - rtol = 2^{-7} = 7.8125e-3 - atol = 1e-2 + 判定公式:|actual - golden| <= atol + rtol * |golden| + + 阈值表: + | 数据类型 | atol | rtol | + |----------|-------|-----------------| + | FLOAT32 | 2e-5 | 2**(-13) | + | FLOAT16 | 5e-3 | 2**(-10) | + | BFLOAT16 | 1e-2 | 2**(-7) | + + 未识别 dtype 走 fp32 默认。 """ import torch - default_tol = { - "rtol": 2**(-13), - "atol": 1e-5, + if isinstance(data_type, str): + return ALLCLOSE_TOLS_STR.get(data_type.lower(), ALLCLOSE_DEFAULT_TOLS) + + dtype_map = { + torch.float16: ALLCLOSE_TOLS_STR["float16"], + torch.bfloat16: ALLCLOSE_TOLS_STR["bfloat16"], + torch.float32: ALLCLOSE_TOLS_STR["float32"], } + return dtype_map.get(data_type, ALLCLOSE_DEFAULT_TOLS) - if isinstance(data_type, str): - key = data_type.lower().replace("torch.", "") - str_to_tol = { - "float32": { - "rtol": 2**(-13), - "atol": 1e-5, - }, - "float": { - "rtol": 2**(-13), - "atol": 1e-5, - }, - "float16": { - "rtol": 2**(-10), - "atol": 1e-3, - }, - "half": { - "rtol": 2**(-10), - "atol": 1e-3, - }, - "bfloat16": { - "rtol": 2**(-7), - "atol": 1e-2, - }, - } - return str_to_tol.get(key, default_tol) - - dtype_to_tol = { - torch.float32: { - "rtol": 2**(-13), - "atol": 1e-5, - }, - torch.float16: { - "rtol": 2**(-10), - "atol": 1e-3, - }, - torch.bfloat16: { - "rtol": 2**(-7), - "atol": 1e-2, - }, + +def _is_integer_dtype(dtype): + """判断 torch.dtype 是否为整数类型(不含 bool / 不含浮点 / 不含复数)。""" + import torch + if dtype == torch.bool: + return False + return (not dtype.is_floating_point) and (not dtype.is_complex) + + +def _build_dtype_rank(): + """dtype 精度优先级表:值越大精度越高。 + 顺序:fp64 > fp32 > fp16 > bf16 > fp8 > int64 > int32 > int16 > int8 > bool + """ + import torch + rank = { + torch.float64: 100, + torch.float32: 90, + torch.float16: 80, + torch.bfloat16: 70, + torch.int64: 50, + torch.int32: 40, + torch.int16: 30, + torch.int8: 20, + torch.uint8: 20, + torch.bool: 10, } + for name in ("float8_e4m3fn", "float8_e4m3", "float8_e5m2fn", "float8_e5m2"): + dt = getattr(torch, name, None) + if dt is not None: + rank[dt] = 60 + return rank + + +_DTYPE_RANK = None + + +def _dtype_rank(dtype): + global _DTYPE_RANK + if _DTYPE_RANK is None: + _DTYPE_RANK = _build_dtype_rank() + return _DTYPE_RANK.get(dtype, 0) + + +def _is_int_like_dtype(dtype): + """判断 dtype 属于"整型类"输入(含 bool;不含浮点/复数)。""" + import torch + if dtype is None: + return False + if dtype == torch.bool: + return True + return (not dtype.is_floating_point) and (not dtype.is_complex) + + +def _infer_input_type(inputs): + """从 inputs 推断输入类型,返回 ("float" | "int" | "no_tensor", input_dtype | None)。 - return dtype_to_tol.get(data_type, default_tol) + 判定优先级(KernelBench / NPUKernelBench 统一处理): + 1. 若存在 torch.Tensor 输入:取所有 tensor 中最高精度 dtype 作为输入类型 + 2. 若不存在 tensor,但存在 list/tuple of Tensor(tensor_list):取第一个 tensor_list 的首元素 dtype + 3. 其他情况(全为标量 attr / 无输入):返回 ("no_tensor", None) + + bool 输入归到 "int" 类(按规则:bool 输出单独处理;bool 输入与 int 同等对待)。 + """ + import torch + tensors = [x for x in inputs if isinstance(x, torch.Tensor)] + source = None + candidate_dtypes = [] + if tensors: + candidate_dtypes = [t.dtype for t in tensors] + top_dtype = max(candidate_dtypes, key=_dtype_rank) + source = "tensor" + else: + tensor_lists = [ + x for x in inputs + if isinstance(x, (list, tuple)) and len(x) > 0 + and all(isinstance(e, torch.Tensor) for e in x) + ] + if tensor_lists: + top_dtype = tensor_lists[0][0].dtype + candidate_dtypes = [top_dtype] + source = "tensor_list" + else: + print( + " [输入类型判定] 来源=无 tensor 输入(全 attr 或空)," + "input_type=no_tensor", + file=sys.stderr, + ) + return "no_tensor", None + + input_type = "int" if _is_int_like_dtype(top_dtype) else "float" + print( + f" [输入类型判定] 来源={source},候选 dtypes={[str(dt) for dt in candidate_dtypes]}," + f"最高精度={top_dtype},input_type={input_type}", + file=sys.stderr, + ) + return input_type, top_dtype def resolve_input_provider(torch_module): @@ -157,16 +264,136 @@ def resolve_input_provider(torch_module): elif hasattr(torch_module, "get_inputs"): return [torch_module.get_inputs()], 1 else: - raise AttributeError("模块必须提供 get_inputs() 或 get_input_groups() 方法") + raise AttributeError( + f"模块必须提供 get_inputs() 或 get_input_groups() 方法" + ) + +def _compare_binary_exact(fw_out, impl_out, data_type): + """非计算类:二进制完全一致比对。 -def compare(fw_out, impl_out, data_type): - """对比框架输出和实现输出。""" + - 浮点 dtype:通过 view-as-int 比较底层 bit pattern,可识别 NaN payload 差异 + - 整型 / bool:直接 torch.equal + - 复数:实部/虚部分别 view-as-int 比较 + """ import torch + fw = fw_out.contiguous().detach().cpu() + impl = impl_out.contiguous() + if isinstance(impl, torch.Tensor): + impl = impl.detach().cpu() + else: + raise AssertionError(f"非计算类实现输出必须是 Tensor,实际为 {type(impl).__name__}") + + if fw.shape != impl.shape: + raise AssertionError( + f"非计算类验证失败,输出形状不一致: framework={fw.shape}, impl={impl.shape}" + ) + if fw.dtype != impl.dtype: + raise AssertionError( + f"非计算类验证失败,输出 dtype 不一致: framework={fw.dtype}, impl={impl.dtype}" + ) + + def _view_int_dtype(dt): + if dt in (torch.float64, torch.complex64): + return torch.int64 + if dt in (torch.float32,): + return torch.int32 + if dt in (torch.float16, torch.bfloat16): + return torch.int16 + for name in ("float8_e4m3fn", "float8_e4m3", "float8_e5m2fn", "float8_e5m2"): + fp8 = getattr(torch, name, None) + if fp8 is not None and dt == fp8: + return torch.int8 + return None + + if fw.dtype.is_complex: + fw_real_bits = torch.view_as_real(fw) + impl_real_bits = torch.view_as_real(impl) + view_dt = _view_int_dtype(torch.float32) if fw.dtype == torch.complex64 else torch.int64 + equal = torch.equal(fw_real_bits.view(view_dt), impl_real_bits.view(view_dt)) + elif fw.dtype.is_floating_point: + view_dt = _view_int_dtype(fw.dtype) + if view_dt is None: + raise AssertionError(f"非计算类不支持的浮点 dtype: {fw.dtype}") + equal = torch.equal(fw.view(view_dt), impl.view(view_dt)) + else: + equal = torch.equal(fw, impl) + + if equal: + return + + if fw.dtype.is_floating_point and not fw.dtype.is_complex: + view_dt = _view_int_dtype(fw.dtype) + fw_bits = fw.view(view_dt).flatten() + impl_bits = impl.view(view_dt).flatten() + diff_mask = fw_bits != impl_bits + violation_count = int(diff_mask.sum().item()) + violation_idx = torch.where(diff_mask)[0] + num_to_show = min(10, len(violation_idx)) + detail = f"前 {num_to_show} 个 bit 不一致位置:\n" + fw_flat = fw.flatten() + impl_flat = impl.flatten() + for i in range(num_to_show): + idx = violation_idx[i].item() + detail += ( + f" 位置[{idx}]: framework={fw_flat[idx].item()} " + f"(bits=0x{fw_bits[idx].item() & ((1 << view_dt.itemsize * 8) - 1):x}), " + f"impl={impl_flat[idx].item()} " + f"(bits=0x{impl_bits[idx].item() & ((1 << view_dt.itemsize * 8) - 1):x})\n" + ) + else: + fw_flat = fw.flatten() + impl_flat = impl.flatten() + diff_mask = fw_flat != impl_flat + violation_count = int(diff_mask.sum().item()) + violation_idx = torch.where(diff_mask)[0] + num_to_show = min(10, len(violation_idx)) + detail = f"前 {num_to_show} 个不一致位置:\n" + for i in range(num_to_show): + idx = violation_idx[i].item() + detail += ( + f" 位置[{idx}]: framework={fw_flat[idx].item()}, " + f"impl={impl_flat[idx].item()}\n" + ) + + metrics = { + "category": "non_compute", + "dtype": str(data_type), + "violation_count": violation_count, + "total_elements": int(fw.numel()), + } + raise AccuracyError( + f"验证失败 dtype={data_type} (非计算类,要求二进制完全一致): " + f"{violation_count}/{fw.numel()} 元素不一致\n{detail}", + metrics, + ) + + +def compare(fw_out, impl_out, data_type, input_type=None, input_dtype=None, non_compute=False): + """对比框架输出和实现输出。 + + Args: + fw_out: 框架(金标准)输出 Tensor + impl_out: 被测实现输出 Tensor + data_type: 输出 dtype(与 fw_out.dtype 一致) + input_type: 输入类型 "float" / "int" / "no_tensor" / None + 由 _infer_input_type() 推断得出,参与"输出整型时"的分流。 + input_dtype: 输入最高精度 dtype(由 _infer_input_type() 返回),仅用于诊断打印。 + non_compute: 若 True,强制走二进制完全一致路径(搬移 / Cast 等算子) + + 决策矩阵(non_compute=False 时): + | 输出 dtype | 输入类型 | 类别 | 判定 | + |-----------|------------------|---------------|--------------------| + | bool | 任意 | bool 输出 | torch.equal | + | int | int | 整数计算类 | |diff| == 0 | + | int | float | 量化类 | |diff| <= 1 | + | int | no_tensor | 整数计算类 | |diff| == 0(最严) | + | float | 任意 | 浮点计算类 | 三项判定(按输出 dtype)| + """ + import torch fw_flat = fw_out.flatten().detach().cpu() impl_flat = impl_out.flatten() - if isinstance(impl_flat, torch.Tensor): impl_flat = impl_flat.detach().cpu() else: @@ -179,6 +406,17 @@ def compare(fw_out, impl_out, data_type): f"验证失败,输出形状不一致: framework={fw_flat.shape}, impl={impl_flat.shape}" ) + # 非计算类:二进制完全一致(先于其他判定,跳过 NaN/Inf/finite 过滤) + if non_compute: + print( + f" [评测模式] 模式=non_compute(非计算类)," + f"输入 dtype={input_dtype}({input_type}),输出 dtype={data_type};" + f"误差要求=二进制完全一致(view-as-int bit pattern 全等,含 NaN payload)", + file=sys.stderr, + ) + _compare_binary_exact(fw_out, impl_out, data_type) + return + fw_nan_mask = torch.isnan(fw_flat) impl_nan_mask = torch.isnan(impl_flat) if not torch.equal(fw_nan_mask, impl_nan_mask): @@ -198,7 +436,6 @@ def compare(fw_out, impl_out, data_type): f"验证失败,Inf 位置不匹配: Framework={fw_inf_count}/{size}, " f"Implementation={impl_inf_count}/{size}" ) - if fw_inf_mask.any(): if not torch.equal( torch.sign(fw_flat[fw_inf_mask]), @@ -208,7 +445,6 @@ def compare(fw_out, impl_out, data_type): finite_mask = torch.isfinite(fw_flat) & torch.isfinite(impl_flat) finite_count = finite_mask.sum().item() - if finite_count == 0: print("警告: 所有值都是非有限值,跳过精度检查") return @@ -216,129 +452,273 @@ def compare(fw_out, impl_out, data_type): fw_finite = fw_flat[finite_mask] impl_finite = impl_flat[finite_mask] + # bool 输出独立处理:严格相等 if fw_finite.dtype == torch.bool: + print( + f" [评测模式] 模式=bool_output(bool 输出)," + f"输入 dtype={input_dtype}({input_type}),输出 dtype={data_type};" + f"误差要求=torch.equal 严格相等(finite={finite_count}/{size})", + file=sys.stderr, + ) if not torch.equal(fw_finite, impl_finite): - raise AssertionError(f"验证失败,布尔值不匹配: dtype={data_type}") + diff_idx = torch.where(fw_finite != impl_finite)[0] + violation_count = int(diff_idx.numel()) + num_to_show = min(10, violation_count) + detail = f"前 {num_to_show} 个不一致位置:\n" + for i in range(num_to_show): + idx = diff_idx[i].item() + detail += ( + f" 位置[{idx}]: framework={fw_finite[idx].item()}, " + f"impl={impl_finite[idx].item()}\n" + ) + metrics = { + "category": "bool_output", + "dtype": str(data_type), + "violation_count": violation_count, + "total_finite": int(fw_finite.numel()), + } + raise AccuracyError( + f"验证失败 dtype={data_type} (bool 输出,要求严格相等): " + f"{violation_count}/{fw_finite.numel()} 元素不一致\n{detail}", + metrics, + ) return + # 输出整型:按 input_type 分流 + if _is_integer_dtype(fw_finite.dtype): + # input_type == "float" → 量化类 (|diff|<=1) + # input_type == "int" 或 "no_tensor" 或 None → 整数计算类 (|diff|==0,最严) + if input_type == "float": + print( + f" [评测模式] 模式=quant_fp_to_int(量化类 fp→int)," + f"输入 dtype={input_dtype}({input_type}),输出 dtype={data_type};" + f"误差要求=|actual - golden| <= 1(finite={finite_count}/{size})", + file=sys.stderr, + ) + diff = (fw_finite.to(torch.int64) - impl_finite.to(torch.int64)).abs() + violation_count = int((diff > 1).sum().item()) + if violation_count > 0: + max_diff = int(diff.max().item()) + violation_idx = torch.where(diff > 1)[0] + num_to_show = min(10, len(violation_idx)) + detail = f"前 {num_to_show} 个量化误差超限位置:\n" + for i in range(num_to_show): + idx = violation_idx[i].item() + detail += ( + f" 位置[{idx}]: framework={fw_finite[idx].item()}, " + f"impl={impl_finite[idx].item()}, " + f"|diff|={diff[idx].item()} (允许<=1)\n" + ) + metrics = { + "category": "quant_fp_to_int", + "dtype": str(data_type), + "input_type": input_type, + "max_abs_diff": max_diff, + "violation_count": violation_count, + "total_finite": int(diff.numel()), + "tolerance": 1, + } + raise AccuracyError( + f"验证失败 dtype={data_type} (量化类 fp->int,要求|diff|<=1): " + f"{violation_count}/{diff.numel()} 元素超限,max_abs_diff={max_diff}\n" + f"{detail}", + metrics, + ) + return + else: + # 整数计算类:严格相等 + print( + f" [评测模式] 模式=integer_compute(整数计算类)," + f"输入 dtype={input_dtype}({input_type}),输出 dtype={data_type};" + f"误差要求=|actual - golden| == 0(严格相等,finite={finite_count}/{size})", + file=sys.stderr, + ) + if not torch.equal(fw_finite, impl_finite): + diff = (fw_finite.to(torch.int64) - impl_finite.to(torch.int64)).abs() + violation_count = int((diff > 0).sum().item()) + max_diff = int(diff.max().item()) + violation_idx = torch.where(diff > 0)[0] + num_to_show = min(10, len(violation_idx)) + detail = f"前 {num_to_show} 个不一致位置:\n" + for i in range(num_to_show): + idx = violation_idx[i].item() + detail += ( + f" 位置[{idx}]: framework={fw_finite[idx].item()}, " + f"impl={impl_finite[idx].item()}, " + f"|diff|={diff[idx].item()}\n" + ) + metrics = { + "category": "integer_compute", + "dtype": str(data_type), + "input_type": input_type, + "max_abs_diff": max_diff, + "violation_count": violation_count, + "total_finite": int(diff.numel()), + "tolerance": 0, + } + raise AccuracyError( + f"验证失败 dtype={data_type} (整数计算类,要求严格相等): " + f"{violation_count}/{diff.numel()} 元素不一致,max_abs_diff={max_diff}\n" + f"{detail}", + metrics, + ) + return + if impl_finite.dtype != fw_finite.dtype: impl_finite = impl_finite.to(fw_finite.dtype) - # 执行 allclose 精度验证 - _check_accuracy_allclose(fw_finite, impl_finite, data_type) + # 输出浮点:按浮点精度标准执行(dtype-aware 三项判定) + sv_thr_pre, sv_err_pre, rel_thr_pre = get_limits(data_type) + atol_pre, rtol_pre = get_allclose_tols(data_type) + print( + f" [评测模式] 模式=float_compute(浮点计算类)," + f"输入 dtype={input_dtype}({input_type}),输出 dtype={data_type};" + f"误差要求=三项 AND:" + f"(1)max_error_cap |diff|<=atol+rtol*|golden| " + f"[atol={atol_pre:.3e}, rtol={rtol_pre:.3e}]," + f"(2)matched_ratio>={REQUIRED_MATCHED_RATIO} " + f"[小值域 sv_thr={sv_thr_pre:.3e}/sv_err={sv_err_pre:.3e}," + f"正常域 rel_thr={rel_thr_pre:.3e}]," + f"(3)MERE<{rel_thr_pre:.3e}(finite={finite_count}/{size})", + file=sys.stderr, + ) + _check_accuracy_npu_benchmark(fw_finite, impl_finite, data_type) + +def _check_accuracy_npu_benchmark(golden, actual, data_type): + """执行 NPU Benchmark 精度验证(三项判定)。 -def _check_accuracy_allclose(golden, actual, data_type): - """执行 allclose 精度验证。 + 元素级 matched 定义(用于 #2 matched_ratio): + - |golden| < small_value_threshold(小值域):|diff| <= small_value_error + - 否则(正常值域):|diff| / (|golden| + 1e-7) <= rel_threshold - 判定标准: - abs(actual - golden) <= atol + rtol * abs(golden) + 通过条件(三项 AND): + 1. allclose: 所有元素满足 |diff| <= atol + rtol * |golden|(dtype-aware) + 2. matched_ratio = sum(matched) / total_finite >= REQUIRED_MATCHED_RATIO(0.9) + 3. MERE < rel_threshold(对所有 finite 元素计算相对误差再取均值, + 分母统一用 |golden| + 1e-7 防除零) Args: - golden: 参考输出,通常是 PyTorch framework 输出 - actual: 被测实现输出,通常是 Triton-Ascend 输出 + golden: 参考输出(金标准) + actual: 被测实现输出 data_type: 数据类型,用于获取对应阈值 Raises: - AssertionError: 当精度验证未通过时 + AccuracyError: 当精度验证未通过时,异常的 metrics 属性携带结构化指标 """ import torch + # 统一升 float32,避免低精度 dtype 自身误差污染计算 golden_f = golden.float() actual_f = actual.float() - if golden_f.shape != actual_f.shape: - raise AssertionError( - f"验证失败,输出形状不一致: golden={golden_f.shape}, actual={actual_f.shape}" - ) + sv_thr, sv_err, rel_thr = get_limits(data_type) + atol, rtol = get_allclose_tols(data_type) + + abs_diff = (actual_f - golden_f).abs() + abs_golden = golden_f.abs() + + # 分桶(用于 #2 matched_ratio) + small_mask = abs_golden < sv_thr + normal_mask = ~small_mask + + # 元素级 matched(#2 口径) + small_ok = abs_diff <= sv_err + rel_err = abs_diff / (abs_golden + 1e-7) + normal_ok = rel_err <= rel_thr + matched_mask = torch.where(small_mask, small_ok, normal_ok) + + total_finite = matched_mask.numel() + matched_count = int(matched_mask.sum().item()) + matched_ratio = matched_count / total_finite if total_finite > 0 else 1.0 + max_abs_diff = abs_diff.max().item() if total_finite > 0 else 0.0 + + # #1 allclose:逐元素判定,要求 100% 通过 + allclose_bound = atol + rtol * abs_golden + allclose_mask = abs_diff <= allclose_bound + allclose_violation_count = int((~allclose_mask).sum().item()) if total_finite > 0 else 0 + allclose_ok = allclose_violation_count == 0 + + # MERE:对所有 finite 元素计算相对误差再取均值(分母统一 |golden| + 1e-7 防除零) + normal_count = int(normal_mask.sum().item()) + if total_finite > 0: + MERE = rel_err.mean().item() + mere_ok = MERE < rel_thr + else: + MERE = None + mere_ok = True - numel = golden_f.numel() - if numel == 0: - return + ratio_ok = matched_ratio >= REQUIRED_MATCHED_RATIO + is_pass = allclose_ok and ratio_ok and mere_ok - tol = get_allclose_tolerance(data_type) - rtol = tol["rtol"] - atol = tol["atol"] + if is_pass: + return - diff = (actual_f - golden_f).abs() - golden_abs = golden_f.abs() + metrics = { + "matched_ratio": matched_ratio, + "max_abs_diff": max_abs_diff, + "MERE": MERE, + "rel_threshold": rel_thr, + "small_value_threshold": sv_thr, + "small_value_error": sv_err, + "atol": atol, + "rtol": rtol, + "max_error_cap_violation_count": allclose_violation_count, + "required_matched_ratio": REQUIRED_MATCHED_RATIO, + "total_finite": total_finite, + "matched_count": matched_count, + "small_count": int(small_mask.sum().item()), + "normal_count": normal_count, + "checks": { + "max_error_cap": allclose_ok, + "required_matched_ratio": ratio_ok, + "MERE": mere_ok, + }, + } - allowed_error = atol + rtol * golden_abs - close_mask = diff <= allowed_error - allclose_ok = bool(close_mask.all().item()) + mere_str = f"{MERE:.6e}" if MERE is not None else "n/a" + error_msg = ( + f"验证失败 dtype={data_type}: " + f"max_error_cap_violations={allclose_violation_count}/{total_finite} " + f"(atol={atol:.6e}, rtol={rtol:.6e}, max_abs_diff={max_abs_diff:.6e}, ok={allclose_ok}), " + f"matched_ratio={matched_ratio:.6f} (req>={REQUIRED_MATCHED_RATIO}, ok={ratio_ok}), " + f"MERE={mere_str} (rel_thr={rel_thr:.6e}, ok={mere_ok}); " + f"small_count={metrics['small_count']}, normal_count={normal_count}\n" + ) + # 仅在对应检查失败时打印各自的违例位置(前 N 个) if not allclose_ok: - failed_close_mask = ~close_mask - failed_close_count = int(failed_close_mask.sum().item()) - pass_rate = 1.0 - failed_close_count / max(numel, 1) - - max_abs_err = diff.max().item() - mean_abs_err = diff.mean().item() - max_allowed_err = allowed_error.max().item() - mean_allowed_err = allowed_error.mean().item() - - # 为了日志可读,计算一个诊断用相对误差。 - # 注意:该 relative_error 只用于错误信息展示,不参与判定。 - rel_denom_floor = atol / rtol - rel_denom = torch.clamp(golden_abs, min=rel_denom_floor) - relative_error = diff / rel_denom - max_rel_err = relative_error.max().item() - mean_rel_err = relative_error.mean().item() - - failed_indices = torch.where(failed_close_mask)[0] - num_failed_to_show = min(10, len(failed_indices)) - - topk = min(10, numel) - top_rel_values, top_rel_indices = torch.topk(relative_error, k=topk) - - error_msg = ( - "验证失败,输出不一致:\n" - f" dtype={data_type}\n" - f" numel={numel}\n" - f" allclose_ok={allclose_ok}\n" - f" pass_rate={pass_rate:.6%}\n" - f" failed_close_count={failed_close_count}/{numel}\n" - "\n" - "阈值配置:\n" - f" rtol={rtol:.12e}\n" - f" atol={atol:.12e}\n" - f" rel_denom_floor=atol/rtol={rel_denom_floor:.12e} # 仅用于日志中的相对误差\n" - "\n" - "误差统计:\n" - f" max_abs_err={max_abs_err:.12e}\n" - f" mean_abs_err={mean_abs_err:.12e}\n" - f" max_rel_err={max_rel_err:.12e} # 仅日志\n" - f" mean_rel_err={mean_rel_err:.12e} # 仅日志\n" - f" max_allowed_err={max_allowed_err:.12e}\n" - f" mean_allowed_err={mean_allowed_err:.12e}\n" - ) - - if failed_close_count > 0: - error_msg += f"\n前 {num_failed_to_show} 个 allclose 失败点:\n" - for i in range(num_failed_to_show): - idx = failed_indices[i].item() - error_msg += ( - f" 位置[{idx}]: " - f"golden={golden_f[idx].item():.12e}, " - f"actual={actual_f[idx].item():.12e}, " - f"abs_err={diff[idx].item():.12e}, " - f"allowed={allowed_error[idx].item():.12e}, " - f"rel_err={relative_error[idx].item():.12e}\n" - ) - - error_msg += f"\n相对误差最大的前 {topk} 个点,注意仅用于诊断,不参与判定:\n" - for i in range(topk): - idx = top_rel_indices[i].item() + allclose_violation_indices = torch.where(~allclose_mask)[0] + num_to_show = min(10, len(allclose_violation_indices)) + error_msg += f"前 {num_to_show} 个 max_error_cap 违例位置:\n" + for i in range(num_to_show): + idx = allclose_violation_indices[i].item() error_msg += ( - f" 位置[{idx}]: " - f"golden={golden_f[idx].item():.12e}, " - f"actual={actual_f[idx].item():.12e}, " - f"abs_err={diff[idx].item():.12e}, " - f"allowed={allowed_error[idx].item():.12e}, " - f"rel_err={relative_error[idx].item():.12e}\n" + f" 位置[{idx}]: framework={golden[idx]:.6e}, " + f"impl={actual[idx]:.6e}, |diff|={abs_diff[idx]:.6e} " + f"(允许<=atol+rtol*|golden|={allclose_bound[idx]:.6e})\n" ) - raise AssertionError(error_msg) + if not ratio_ok: + unmatched_mask = ~matched_mask + unmatched_indices = torch.where(unmatched_mask)[0] + num_to_show = min(10, len(unmatched_indices)) + error_msg += f"前 {num_to_show} 个 matched 未通过位置:\n" + for i in range(num_to_show): + idx = unmatched_indices[i].item() + if small_mask[idx].item(): + error_msg += ( + f" 位置[{idx}] (小值域): framework={golden[idx]:.6e}, " + f"impl={actual[idx]:.6e}, |diff|={abs_diff[idx]:.6e} " + f"(允许<={sv_err:.6e})\n" + ) + else: + error_msg += ( + f" 位置[{idx}] (正常域): framework={golden[idx]:.6e}, " + f"impl={actual[idx]:.6e}, 相对误差={rel_err[idx]:.6e} " + f"(允许<={rel_thr:.6e})\n" + ) + raise AccuracyError(error_msg, metrics) def run_single_case( @@ -348,12 +728,16 @@ def run_single_case( device, case_idx, total_cases, + non_compute=False, ): """验证单组输入。失败时抛出 AssertionError。""" import torch print(f" 测试第 {case_idx}/{total_cases} 组输入...", file=sys.stderr) + # 推断输入类型("float" / "int" / "no_tensor")→ 决定输出整型时走整数计算 vs 量化 + input_type, input_dtype = _infer_input_type(inputs) + inputs_for_impl = [ x.to(device) if isinstance(x, torch.Tensor) else x for x in inputs @@ -378,37 +762,45 @@ def run_single_case( f"framework={len(framework_output)}, impl={len(impl_output)}" ) + print( + f" [输出概览] 共 {len(framework_output)} 个输出,non_compute={non_compute}", + file=sys.stderr, + ) + for i, (fw_out, impl_out) in enumerate(zip(framework_output, impl_output)): if fw_out is None or impl_out is None: raise AssertionError( f"[用例 {case_idx}/{total_cases}] 输出 {i} 为 None: " f"framework={fw_out is None}, impl={impl_out is None}" ) - if isinstance(fw_out, torch.Tensor) and isinstance(impl_out, torch.Tensor): try: data_type = fw_out.dtype - compare(fw_out, impl_out, data_type) - except AssertionError as e: - raise AssertionError(f"[用例 {case_idx}/{total_cases}] 输出 {i}: {str(e)}") from e - else: - if fw_out != impl_out: - raise AssertionError( - f"[用例 {case_idx}/{total_cases}] 输出 {i} 非 Tensor 值不一致: " - f"framework={fw_out}, impl={impl_out}" + print( + f" [输出 {i}] shape={list(fw_out.shape)}, dtype={data_type}", + file=sys.stderr, + ) + compare( + fw_out, impl_out, data_type, + input_type=input_type, input_dtype=input_dtype, + non_compute=non_compute, ) + except AccuracyError as e: + raise AccuracyError( + f"[用例 {case_idx}/{total_cases}] {str(e)}", e.metrics + ) from e + except AssertionError as e: + raise AssertionError(f"[用例 {case_idx}/{total_cases}] {str(e)}") from e -def verify_implementations( - op_name, - verify_dir, - triton_impl_name="triton_ascend_impl", - output_path=None, -): +def verify_implementations(op_name, verify_dir, triton_impl_name="triton_ascend_impl", output_path=None, non_compute=False): """验证框架实现和生成实现的结果一致性。 每个 shape 独立 try/except,全部跑完后写 verify_result.json。 + Args: + non_compute: 若 True,所有 case 走"非计算类"二进制完全一致判定(搬移/Cast 等算子) + Returns: (passed_cases, total_cases) """ @@ -439,10 +831,8 @@ def verify_implementations( input_desc = describe_input(inputs) framework_model = None impl_model = None - try: init_params = get_init_inputs() - torch.manual_seed(0) torch.npu.manual_seed(0) framework_model = FrameworkModel(*init_params).to(device) @@ -452,28 +842,22 @@ def verify_implementations( impl_model = ModelNew(*init_params).to(device) run_single_case( - framework_model, - impl_model, - inputs, - device, - case_idx, - total_cases, + framework_model, impl_model, inputs, device, case_idx, total_cases, + non_compute=non_compute, ) passed_cases += 1 - except Exception as e: err_detail = traceback.format_exc() - print( - f" [用例 {case_idx}/{total_cases}] 失败: {type(e).__name__}: {e}", - file=sys.stderr, - ) - failures.append({ + print(f" [用例 {case_idx}/{total_cases}] 失败: {type(e).__name__}: {e}", file=sys.stderr) + failure_entry = { "case_idx": case_idx, "input_desc": input_desc, "error_type": type(e).__name__, "error_msg": truncate_error(err_detail), - }) - + } + if isinstance(e, AccuracyError): + failure_entry["metrics"] = e.metrics + failures.append(failure_entry) finally: del framework_model del impl_model @@ -481,9 +865,9 @@ def verify_implementations( failed_cases = total_cases - passed_cases + # 落盘 verify_result.json if output_path is None: output_path = os.path.join(verify_dir, "verify_result.json") - result = { "op_name": op_name, "total_cases": total_cases, @@ -491,7 +875,6 @@ def verify_implementations( "failed_cases": failed_cases, "failures": failures, } - try: with open(output_path, "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) @@ -515,30 +898,22 @@ def verify_implementations( parser = argparse.ArgumentParser(description="算子验证脚本") parser.add_argument("--op_name", required=True, help="算子名称") parser.add_argument( - "--verify_dir", - default=".", - help=( - "验证目录,包含 {op_name}_torch.py 和 " - "{op_name}_triton_ascend_impl.py(默认当前目录)" - ), + "--verify_dir", default=".", + help="验证目录,包含 {op_name}_torch.py 和 {op_name}_triton_ascend_impl.py(默认当前目录)", ) - parser.add_argument("--timeout", type=int, default=900, help="超时秒数(默认 900)") + parser.add_argument("--timeout", type=int, default=900, help="超时秒数(已忽略:当前为同进程串行模式)") parser.add_argument( - "--triton_impl_name", - default="triton_ascend_impl", + "--triton_impl_name", default="triton_ascend_impl", help="Triton 实现模块名(不含 op_name 前缀,默认 triton_ascend_impl)", ) parser.add_argument( - "--output", - default=None, + "--output", default=None, help="验证结果 JSON 输出路径(默认 {verify_dir}/verify_result.json)", ) parser.add_argument( - "--_run", - action="store_true", - help=argparse.SUPPRESS, + "--non-compute", action="store_true", + help="非计算类算子(搬移 / Cast 等),所有 case 走二进制完全一致判定", ) - args = parser.parse_args() verify_dir = os.path.abspath(args.verify_dir) @@ -546,57 +921,14 @@ def verify_implementations( print(f"错误: 验证目录不存在: {verify_dir}", file=sys.stderr) sys.exit(1) - if args._run: - # 子进程模式:直接执行验证逻辑 - try: - passed, total = verify_implementations( - args.op_name, - verify_dir, - args.triton_impl_name, - args.output, - ) - except Exception as e: - print(f"{e}", file=sys.stderr) - traceback.print_exc() - sys.exit(1) - - # 策略 A:passed < total → exit 1 - sys.exit(0 if passed == total and total > 0 else 1) - - else: - # 主进程模式:启动子进程执行验证,超时后 kill 子进程 - cmd = [ - sys.executable, - os.path.abspath(__file__), - "--op_name", - args.op_name, - "--verify_dir", - verify_dir, - "--triton_impl_name", - args.triton_impl_name, - "--_run", - ] - - if args.output: - cmd.extend(["--output", args.output]) - - try: - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = proc.communicate(timeout=args.timeout) - - sys.stdout.buffer.write(stdout) - sys.stdout.buffer.flush() - sys.stderr.buffer.write(stderr) - sys.stderr.buffer.flush() - - sys.exit(proc.returncode) - - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - print(f"验证超时({args.timeout}秒),已终止子进程", file=sys.stderr) - sys.exit(1) + try: + passed, total = verify_implementations( + args.op_name, verify_dir, args.triton_impl_name, args.output, + non_compute=args.non_compute, + ) + except Exception as e: + print(f"{e}", file=sys.stderr) + traceback.print_exc() + sys.exit(1) + # 策略 A:passed < total → exit 1 + sys.exit(0 if passed == total and total > 0 else 1) \ No newline at end of file diff --git a/utils/run_benchmark_triton.sh b/utils/run_benchmark_triton.sh index a1cd982f..95175846 100644 --- a/utils/run_benchmark_triton.sh +++ b/utils/run_benchmark_triton.sh @@ -210,6 +210,9 @@ if [[ "$USE_PARALLEL" == true ]]; then mkdir -p "$TARGET_OP_DIR" + # 预生成 session-id,调用后按 SID 精确取 jsonl,避免 ls -t 竞态 + SID=$(python3 -c 'import uuid;print(uuid.uuid4())') + START_TIME=$(date +%s) if [[ -f "$json_file" ]]; then @@ -219,6 +222,7 @@ if [[ "$USE_PARALLEL" == true ]]; then fi if claude -p "$PROMPT" \ + --session-id "$SID" \ --allowedTools 'Bash(*)' 'Read(*)' 'Write(*)' 'Edit(*)' 'Glob(*)' 'Grep(*)' 'Skill(*)' \ >> "${OUTPUT_DIR}/npu_${npu}.log" 2>&1; then @@ -251,19 +255,16 @@ if [[ "$USE_PARALLEL" == true ]]; then STATUS="fail" fi - # 串行重命名思维轨迹文件(带时间戳防止同名覆盖) - { - flock -x 201 - LATEST_JSONL=$(ls -t "$CLAUDE_PROJECT_DIR"/*.jsonl 2>/dev/null | head -1) - if [[ -n "$LATEST_JSONL" && -f "$LATEST_JSONL" ]]; then - BASENAME=$(basename "$LATEST_JSONL" .jsonl) - TIMESTAMP=$(date +%Y%m%d_%H%M%S) - mv "$LATEST_JSONL" "${CLAUDE_PROJECT_DIR}/${op_name}_${STATUS}_${TIMESTAMP}.jsonl" - if [[ -d "${CLAUDE_PROJECT_DIR}/${BASENAME}" ]]; then - mv "${CLAUDE_PROJECT_DIR}/${BASENAME}" "${CLAUDE_PROJECT_DIR}/${op_name}_${STATUS}_${TIMESTAMP}" - fi - fi - } 201>"${OUTPUT_DIR}/.trace_lock" + # 按 session-id 精确搬运思维轨迹(无需 flock,无竞态) + SRC_JSONL="${CLAUDE_PROJECT_DIR}/${SID}.jsonl" + if [[ -f "$SRC_JSONL" ]]; then + mv "$SRC_JSONL" "${TARGET_OP_DIR}/session.jsonl" + else + echo "[NPU $npu] ⚠ 未找到 session jsonl: ${SRC_JSONL}" >&2 + fi + if [[ -d "${CLAUDE_PROJECT_DIR}/${SID}" ]]; then + mv "${CLAUDE_PROJECT_DIR}/${SID}" "${TARGET_OP_DIR}/session_dir" + fi done # ========== Worker 进程结束 ========== ) & @@ -310,6 +311,9 @@ else START_TIME=$(date +%s) + # 预生成 session-id,调用后按 SID 精确取 jsonl + SID=$(python3 -c 'import uuid;print(uuid.uuid4())') + if [[ -f "$json_file" ]]; then PROMPT="生成一个基于 Triton-Ascend 框架的算子,参考${file}和${json_file}。目标设备架构为${ARCH},使用NPU=${NPU_ID},请将生成的代码文件输出至${TARGET_OP_DIR}/目录下。" else @@ -317,6 +321,7 @@ else fi if claude -p "$PROMPT" \ + --session-id "$SID" \ --allowedTools 'Bash(*)' 'Read(*)' 'Write(*)' 'Edit(*)' 'Glob(*)' 'Grep(*)' 'Skill(*)'; then END_TIME=$(date +%s) ELAPSED=$((END_TIME - START_TIME)) @@ -333,15 +338,15 @@ else STATUS="fail" fi - # 重命名思维轨迹文件(带时间戳防止同名覆盖) - LATEST_JSONL=$(ls -t "$CLAUDE_PROJECT_DIR"/*.jsonl 2>/dev/null | head -1) - if [[ -n "$LATEST_JSONL" && -f "$LATEST_JSONL" ]]; then - BASENAME=$(basename "$LATEST_JSONL" .jsonl) - TIMESTAMP=$(date +%Y%m%d_%H%M%S) - mv "$LATEST_JSONL" "${CLAUDE_PROJECT_DIR}/${op_name}_${STATUS}_${TIMESTAMP}.jsonl" - if [[ -d "${CLAUDE_PROJECT_DIR}/${BASENAME}" ]]; then - mv "${CLAUDE_PROJECT_DIR}/${BASENAME}" "${CLAUDE_PROJECT_DIR}/${op_name}_${STATUS}_${TIMESTAMP}" - fi + # 按 session-id 精确搬运思维轨迹 + SRC_JSONL="${CLAUDE_PROJECT_DIR}/${SID}.jsonl" + if [[ -f "$SRC_JSONL" ]]; then + mv "$SRC_JSONL" "${TARGET_OP_DIR}/session.jsonl" + else + echo "[NPU ${NPU_ID}] ⚠ 未找到 session jsonl: ${SRC_JSONL}" + fi + if [[ -d "${CLAUDE_PROJECT_DIR}/${SID}" ]]; then + mv "${CLAUDE_PROJECT_DIR}/${SID}" "${TARGET_OP_DIR}/session_dir" fi done fi