Skip to content

Commit 48e1bf8

Browse files
author
peng.li24
committed
docs: update AGENTS.md
1 parent c47950e commit 48e1bf8

1 file changed

Lines changed: 45 additions & 65 deletions

File tree

AGENTS.md

Lines changed: 45 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
# Agent Instructions for numpycpp
1+
# Agent Instructions
22

33
## 零容错原则 — 暴露错误,禁止吞错
44

5-
**为什么暴露错误这么重要:**
6-
75
吞错误(fallback / silent failure / 返回假值)会导致查找一个很小的 ULP 问题耗费无穷漫长的时间和人力。
86

9-
真实案例:`blas_bridge.h` 中的 `find_openblas_path()` 匹配到了 scipy 的 LP64
10-
OpenBLAS 而非 numpy 的 ILP64 OpenBLAS。`dlsym("dgesv_64_")` 返回 nullptr,
11-
但代码静默 fallback 到 `return false` / 返回零矩阵,导致 5000 个矩阵的 inv
12-
全部错误 —— 没有任何异常、没有任何报错。排查这个 0‑ULP 偏差的根因花费了数周。
13-
147
**规则:**
158

16-
1. **BLAS / LAPACK 符号解析失败 → 立刻 `throw std::runtime_error`,进程退出。**
9+
1. **外部符号解析失败 → 立刻 `throw std::runtime_error`,进程退出。**
1710
不返回 nullptr,不 fallback 到纯 C++ 循环,不返回假值。
1811

19-
2. **任何系统级前提条件不满足(OpenBLAS 未加载、ILP64 ABI 不匹配、
12+
2. **任何系统级前提条件不满足(动态库未加载、ABI 不匹配、
2013
符号缺失)→ 立刻 throw,附带清晰的错误消息说明原因和修复方法。**
2114

2215
3. **禁止 silent failure。** 一个静默错误浪费的排查时间远超一个清晰的
@@ -25,82 +18,69 @@ OpenBLAS 而非 numpy 的 ILP64 OpenBLAS。`dlsym("dgesv_64_")` 返回 nullptr
2518
4. **数学意义上的失败(如奇异矩阵)也要 throw**,不要 return false/zero。
2619
调用方如果期望处理奇异矩阵,应当显式 catch。
2720

28-
## 代码风格
29-
30-
- BLAS 函数指针用 function-local static 缓存,首次调用时通过 `resolve_blas()` 解析。
31-
`resolve_blas()` 要么返回有效指针要么 throw——绝无 nullptr 返回。
32-
- 不做 fallback。`blas_bridge.h` 不存在任何 "if BLAS unavailable, do X" 的代码路径。
21+
## C++ 绑定与 Python 库的对齐规则
3322

34-
## C++ API 与 numpy 严格对齐规则
23+
**核心原则:C++ 绑定的接口和实现必须与目标 Python 库完全一致。
24+
测试代码是"黄金标准"——它反映 Python 库的真实行为。禁止通过修改测试来绕过 C++ 实现缺陷。**
3525

36-
**核心原则:C++ API 必须与 numpy 在接口层面完全一致。禁止为绕过对齐而修改测试代码。**
26+
### 规则 1:参数顺序必须与目标库一致
3727

38-
### 1. 参数顺序必须与 numpy 一致
28+
- **错误:** C++ 绑定擅自调换参数顺序(如把数组放在 mask 前面)
29+
- **正确:** 严格匹配目标库的参数位置
30+
- **实例:** `compress(arr, mask)``compress(condition, a)`
3931

40-
- **错误示例:** `compress(arr, mask)` — C++ 把数组放前面
41-
- **正确:** `compress(condition, a)` — 严格匹配 `numpy.compress(condition, a)`
32+
### 规则 2:默认值语义必须一致,禁止自定义哨兵值
4233

43-
### 2. 默认值语义必须与 numpy 一致
34+
- **错误:**`-1` 表示"未传入参数"——目标库用 `None` 表示
35+
- **正确:** 使用 `py::none()` 匹配目标库的 `None` 语义
36+
- **实例:** `eye(N, M=-1, k=0)``eye(N, M=py::none(), k=0)`
37+
- **规则:** 目标库用什么哨兵,C++ 就用什么。不要发明自己的。
4438

45-
- **错误示例:** `eye(N, M=-1, k=0)` — 用 `-1` 作哨兵值表示"使用 N"
46-
- **正确:** `eye(N, M=py::none(), k=0)` — 匹配 `numpy.eye(N, M=None, k=0)`
47-
- **规则:** 禁止使用自定义哨兵值(如 `-1`)。如果 numpy 用 `None`,C++ 必须用 `py::none()`
39+
### 规则 3:数学函数必须经 dlsym 桥接,禁止直接调用 std::
4840

49-
### 3. 数学函数必须经 SVML 桥,禁止直接调用 std::
41+
- **禁止:** `std::pow()` / `std::atan2()` / `std::exp()` 等 libm 函数直接调用
42+
- **原因:** 目标库使用的 Intel SVML / 自定义多项式实现与 libm 差 1-3 ULP
43+
- **正确:** 通过 `dlsym` 从目标库的 .so 中解析它实际使用的数学符号,在 C++ 侧调用
5044

51-
- **禁止:** `std::pow(base, x)` / `std::atan2(y, x)` / `std::exp(x)` — 这些与 numpy 的 SVML 实现差 1-3 ULP
52-
- **正确:** 通过 `detail::pow()` / `detail::atan2()` / `detail::exp()` 调用 — 这些在 bit_exact 模式下经 dlsym 解析 numpy 的 SVML 向量符号
45+
### 规则 4:dlsym 解析符号必须用目标库实际使用的版本
5346

54-
### 4. SVML 符号必须用向量版本,不能用标量 fallback
47+
- **错误:** `dlsym("npy_atan2")` → 解析到 libm 的标量 `atan2`(差 1 ULP)
48+
- **正确:** `dlsym("__svml_atan28")` → 解析到目标库实际使用的向量实现
49+
- **方法:**`nm -D <目标库.so> | grep <函数名>` 查看目标库实际导出了什么符号,
50+
选择与目标库运行时路径一致的版本
5551

56-
- **错误:** `resolve_svml("npy_atan2")` — 解析到 libm 的标量 `atan2`,与 numpy 内部使用的 `__svml_atan28` 差 1 ULP
57-
- **正确:** `resolve_svml("__svml_atan28")` — 解析 numpy 实际使用的 Intel SVML 向量符号
58-
- **关键符号对照表:**
52+
### 规则 5:头文件 include 顺序
5953

60-
| 函数 | 错误符号(标量 libm) | 正确符号(Intel SVML) |
61-
|------|----------------------|----------------------|
62-
| pow f64 | `npy_pow` | `__svml_pow8` |
63-
| pow f32 | `npy_powf` | `__svml_powf16` |
64-
| atan2 f64 | `npy_atan2` | `__svml_atan28` |
65-
| atan2 f32 | `npy_atan2f` | `__svml_atan2f16` |
54+
内部实现头文件(如数学后端桥接)必须在所有依赖它的模块头文件之前加载。
55+
否则深层模块无法使用桥接符号,可能静默 fallback 到 `std::`
6656

67-
SVML 向量函数签名:`__m512d (*)(__m512d, __m512d)` (f64) / `__m512 (*)(__m512, __m512)` (f32)。
68-
调用方式:标量 → `_mm512_set1_pd(x)` 广播 → SVML → `_mm512_cvtsd_f64()` 提取。
57+
### 规则 6:修复流程
6958

70-
### 5. 头文件 include 顺序
59+
当发现对齐测试失败时:
7160

72-
`numpy.h` 必须在所有子模块头文件之前加载 `svml_bridge.h`,确保 `detail::pow` 等符号在 `init.h``logspace` 等函数中可用。
61+
1. **诊断:** 用最小复现脚本确认差异量级(ULP)
62+
2. **定位根因:** 追溯 C++ 调用链,找出哪一步使用了非等价的实现(参数错误、`std::` 函数、错误的 dlsym 符号等)
63+
3. **修复 C++ 源码:** 修改实现使其严格匹配目标库的等效路径
64+
4. **严禁改测试:** 测试反映目标库的真实行为。修改测试来绕过 C++ 缺陷等同于作弊
7365

74-
### 6. 修复流程:诊断 → 定位根因 → 修 C++ → 严禁改测试
66+
### 规则 7:不重复造轮子
7567

76-
当发现 bit_exact 测试失败时:
77-
78-
1. **诊断:** `python3 -c "import numpycpp, numpy as np; ..."` 确认差异和 ULP 量级
79-
2. **定位根因:** 追溯 C++ 调用链,找出哪一步使用了非 numpy 等价的实现(`std::`、标量 `npy_*`、错误参数顺序等)
80-
3. **修 C++ 源码:** 修改头文件中的实现,使其严格匹配 numpy 的等效路径
81-
4. **严禁改测试:** 测试代码是"黄金标准"——它反映 numpy 的真实行为。修改测试来绕过 C++ 实现缺陷等同于作弊
68+
- 在请求新增 API 前,先确认 **目标库或 pybind11 是否已有等价功能**
69+
- 示例:`py::isinstance<py::array_t<float>>(arr)` 可直接判断 dtype,不需要自己写 `is_float32()`
70+
- 示例:`astype(arr, "float64")` 已实现类型转换,不需要写 `ensure_float64()`
8271

8372
## 编译模式
8473

85-
### bit_exact 模式(默认)
86-
87-
```cmake
88-
# cmake -S tests -B tests/build
89-
```
74+
### 精确模式(默认)
9075

9176
关键 flag:
92-
- `-O2 -ffp-contract=off` — 禁用 FMA 融合
93-
- `-fno-builtin-{exp,log,sin,cos,tan,pow,atan2,sqrt,...}` — 阻止 GCC 替换 dlsym 路径
94-
- `-msse4.1 -mfma` — SIMD 支持
77+
- `-O2 -ffp-contract=off` — 禁用 FMA 融合,确保每一步计算独立
78+
- `-fno-builtin-{exp,log,sin,cos,tan,pow,atan2,sqrt,...}` — 阻止编译器用内置实现替换 dlsym 路径
9579
-`-march=native` — 避免编译器自动矢量化改变计算顺序
96-
- 通过 `dlsym` 从 numpy 的 `_multiarray_umath.so` 解析 SVML 符号
97-
98-
### std 模式
80+
- 通过 `dlsym` 从目标库的 .so 解析数学符号
9981

100-
```cmake
101-
# cmake -S tests -B tests/build_std -DNUMPYCPP_STD_ONLY=ON
102-
```
82+
### 性能模式
10383

104-
- `-O3 -march=native`无精度约束,允许编译器自由优化
105-
-`-fno-builtin-*`使用 GCC 内置数学函数
106-
- 与 numpy 的 SVML 实现预期差 0-2 ULP
84+
- `-O3 -march=native` — 允许编译器自由优化
85+
-`-fno-builtin-*`使用编译器内置数学函数
86+
- 与目标库预期差 0-2 ULP(可接受范围)

0 commit comments

Comments
 (0)