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
22153 . ** 禁止 silent failure。** 一个静默错误浪费的排查时间远超一个清晰的
@@ -25,82 +18,69 @@ OpenBLAS 而非 numpy 的 ILP64 OpenBLAS。`dlsym("dgesv_64_")` 返回 nullptr
25184 . ** 数学意义上的失败(如奇异矩阵)也要 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