Upload docs/porting_notes.md
Browse files- docs/porting_notes.md +173 -0
docs/porting_notes.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# torch → Jittor 移植实战笔记(PLAN §8 的实测版)
|
| 2 |
+
|
| 3 |
+
> 全部条目都在本仓库实际踩过/验证过,L1/L2 测试锁定。给后来者(含 Agent B)参考。
|
| 4 |
+
|
| 5 |
+
## 🔴 会静默出错的(最危险,症状不指向根因)
|
| 6 |
+
|
| 7 |
+
| # | 问题 | 症状 | 对策 |
|
| 8 |
+
|---|---|---|---|
|
| 9 |
+
| 0a | **`.stop_grad()` 是就地打标记(≠torch detach)** | 把它当 detach 用 → 原变量本体被标无梯度,前向全对、相关 loss 梯度静默为 0 | detach 语义一律用 `.detach()`(返回新变量);`.stop_grad()` 只用于"这个变量从此不需要梯度"(如冻结参数) |
|
| 10 |
+
| 0b | **任何 `for x in tensor` 式 python 循环建图 = O(N) 图节点** | 密集批次 grad/sync 阶段 100% CPU 停滞分钟级、位置随 shuffle 漂移、无报错。实锤案例:底座 `diag3d`(循环 jt.diag,gwd_loss 每 iter ×3)、逐实例 voronoi、逐实例 nonzero | 一律批量闭式改写(stack/where/searchsorted 分桶);detach 路径转 numpy;用 `jt.liveness_info()` 打点确认 lived_ops 有界 |
|
| 11 |
+
| 0c | **多进程 dataloader 环形缓冲死锁** | worker 卡 buffer.send、主进程卡 idqueue.pop(py-spy 可证) | num_workers=0(加载占比小时性价比最高) |
|
| 12 |
+
| 1 | **jittor 1.3.8.5 + numpy≥2** | 凡 `jt.array`(numpy 数据)喂进计算图的算子输出未初始化内存;roundtrip 与无输入算子(rand/ones)正常 | 钉死 numpy==1.26.4;smoke 测试有防回归断言 |
|
| 13 |
+
| 2 | **`jt.linalg.{det,inv,eigh,svd}` 是 numpy_code 实现** | GPU 上要 cupy(未装则 ModuleNotFoundError,且因惰性求值报错位置在千里之外的第一次 sync 处) | 2×2 全部用闭式解(`ops/linalg2x2.py`、`det_2x2`) |
|
| 14 |
+
| 3 | **in-place 赋值的自动微分不可靠** | 前向对、梯度 0 | loss 路径一律 out-of-place(jt.where/concat 重建);L2 必须比梯度 |
|
| 15 |
+
| 4 | **`x.max()`/`.sum()` 等 reduce 返回 shape [1] 而非标量** | `jt.stack([a.max(), b.max()])` 得 (2,1),下游 shape 错乱或 concat 报错 | 用 `jt.concat` 拼 reduce 结果 |
|
| 16 |
+
| 5 | **GPU 的 `jt.matmul` 不广播 batch 维**(cublas_batched_matmul) | CPU 通过、GPU 报 Wrong inputs | batch 维不一致时显式 `expand`(见 solve_2x2) |
|
| 17 |
+
| 6 | **conda 26.x `env config vars` 导出成大写** | jittor 读小写 `cc_path`,静默不生效 | 用 `etc/conda/activate.d/*.sh` |
|
| 18 |
+
|
| 19 |
+
## 🟡 语义差异(有对应物但行为不同)
|
| 20 |
+
|
| 21 |
+
| torch/mm 系 | jittor/JDet | 差异 |
|
| 22 |
+
|---|---|---|
|
| 23 |
+
| `torch.meshgrid(x, y, indexing='xy')` | 无 indexing 参数 | 手工构造:`X = x[None,:].expand((ny,nx))` |
|
| 24 |
+
| mmengine `LinearParamScheduler` | — | **分母是 end-begin-1**(≠torch LinearLR),第 W-1 iter 到顶 |
|
| 25 |
+
| mmcv `RoIAlignRotated(clockwise=True)` | JDet 原生方向 | **JDet 原生 = mmcv clockwise=True**(kernel 旋转矩阵互为转置);aligned=-0.5 偏移需另加 |
|
| 26 |
+
| mmcv `nms_rotated` keep 按分数降序 | jdet 返回原始顺序 | head 里先 argsort 再 NMS |
|
| 27 |
+
| `torch.unique(return_inverse/counts)` | jt.unique 语义不保证 | detach 量一律走 numpy |
|
| 28 |
+
| `Tensor.index_reduce_('amin'/'mean')` | 无 | amin 于 unique 分组=组内代表(scatter);mean 带梯度 → one-hot 矩阵乘 |
|
| 29 |
+
| `F.smooth_l1_loss(beta=)` | jt.nn.smooth_l1_loss 无 beta | 手写 `_smooth_l1` |
|
| 30 |
+
| torchvision `resized_crop`(antialias) | jt.nn.interpolate 无 antialias | 已记录差异(增广路径) |
|
| 31 |
+
| `nn.GroupNorm(requires_grad=)` | 无此参数 | config 适配层剥离该键 |
|
| 32 |
+
| mmengine cfg 的 `axis=` 遗留 | jt.concat 只认 `dim=` | 底座 coder.py 已修 |
|
| 33 |
+
| 底座 `RotatedResize` | mmdet Resize | 底座经 poly 往返会重规范角度(点框 0→-π/2);自写 MMRotateResize |
|
| 34 |
+
| 底座 `RotatedRandomFlip` | mmdet RandomFlip | 底座 x'=W-x-1 且角度 π-a;mmrotate 是 W-x 与 -a;自写 MMRotateRandomFlip |
|
| 35 |
+
| 底座 `WhollyWoodDOTADataset` point_dummy=0.1 | 官方 ConvertWeakSupervision 默认 1 | v2 数据集显式用 1 |
|
| 36 |
+
|
| 37 |
+
## 🟢 直接可用(已验证)
|
| 38 |
+
|
| 39 |
+
`jt.concat/stack/split/gather/flip/clamp/where/arange/linspace/permute` /
|
| 40 |
+
`nn.grid_sample(含 reflection)` / `nn.affine_grid` / `nn.interpolate(bilinear, align_corners)` /
|
| 41 |
+
`nn.GroupNorm/PixelShuffle/ConvTranspose2d/MaxPool2d` / `jt.optim.AdamW(+clip_grad_norm 全局 L2)`。
|
| 42 |
+
|
| 43 |
+
## 数值/梯度专题
|
| 44 |
+
|
| 45 |
+
- **2×2 eigh 闭式解**(`ops/linalg2x2.py`):`λ± = mean ± sqrt(clamp(delta²+b², 1e-24))`;
|
| 46 |
+
EPS 取 1e-24(1e-12 会污染面积 1e-6 的框);b 对称化读取让梯度在 [0,1]/[1,0] 对称分摊
|
| 47 |
+
(对齐 torch.linalg.eigh 的梯度约定);`|delta|/disc ≤ 1` ⇒ 特征值梯度天然有界,w==h 不炸。
|
| 48 |
+
- **简并(特征值重复)时**特征基与逐特征值子梯度不唯一:与 torch 的差异只在 a/c 间分配,
|
| 49 |
+
trace 不变量一致。parity 测试对简并用例只比基无关量。
|
| 50 |
+
- **PSC decode ±π/2 端点**:atan2(∓0,-1) 符号翻转给出 +π/2 vs -π/2,le90 周期 π 下等价,
|
| 51 |
+
按模 π 角距离比较。
|
| 52 |
+
- **diff_iou_rotated 的鞋带公式必须在中心化坐标上算**(2026-07-26,stage-2 parity 揪出):
|
| 53 |
+
mmcv 在原始图像坐标(~1e2-1e3)上算,x_i·y_j 项达 1e4-1e6,微小/退化交集依赖正负大项
|
| 54 |
+
相消;jittor 编译器把 `a*d - b*c` 融合成 FMA 后两项舍入不再互为相反数,残差 ~1e-2 px²
|
| 55 |
+
变成假面积(golden 上 loss_bbox 差 16%,torch eager 恰好精确抵消得 0)。修法:排序仍在
|
| 56 |
+
归一化坐标做(不变),面积改在「减均值后再乘 mask 归零填充槽」的坐标上算——闭合多边形
|
| 57 |
+
鞋带公式平移不变,且填充槽零贡献保持。torch 侧原始坐标的噪声底 ~1e-3 仍留在 golden 的
|
| 58 |
+
loss_bbox 梯度里,故 FCOS head 梯度紧验走 cls+ctr 支路(1e-4),总梯度 5e-3。
|
| 59 |
+
- **jittor nn.GroupNorm 是一遍式方差** `E[x²]−E[x]²`(灾难性消去风险),torch 是两遍式
|
| 60 |
+
`E[(x−mean)²]`。底座 BRICKS 'GN' 已换成 `GroupNorm2Pass`(modules.py,签名/参数名兼容)。
|
| 61 |
+
- **golden 若在 CUDA torch 上 dump,必须关 TF32**:torch 2.x 默认 `cudnn.allow_tf32=True`,
|
| 62 |
+
卷积只有 10 位尾数,前向自带 ~4e-4 相对误差,会整体污染 golden(曾被误判为 GN/实现差异,
|
| 63 |
+
实际逐层 CPU 对比 conv 输出 rel 1e-7)。dump 脚本统一加
|
| 64 |
+
`torch.backends.cudnn.allow_tf32 = False; torch.backends.cuda.matmul.allow_tf32 = False`。
|
| 65 |
+
- **torch `ReLU(inplace=True)` 会篡改中间量取证**:dump 中间激活时先 `.clone()` 再过激活,
|
| 66 |
+
否则 pre-act 张量被原地覆盖,逐层对比出现「norm 层 rel=1.0 而 act 后 rel=1e-7」的假象。
|
| 67 |
+
- **GPU 数值 parity 不可用于 bisect / 逐元素断言**(issue #3 结案,2026-07-26 22:00):
|
| 68 |
+
jittor conv 前向/反向用 `cudnnFindConvolution*AlgorithmEx` **真实计时**选算法并做
|
| 69 |
+
**进程内 cache**;cudnn8 在 A100 上 `CUDNN_DEFAULT_MATH` 允许 TF32 tensor-core 算法
|
| 70 |
+
参与候选。选中 TF32 的进程 GPU-vs-golden 前向 ~4e-4、梯度 rel_l2 ~4e-2(逐元素违约
|
| 71 |
+
~22%);选中 FMA 的进程 ~1e-4。结果「进程内确定、跨进程漂移」,且**换 commit 改图形状
|
| 72 |
+
会扰动 benchmark 计时** → bisect 会把算法选择的翻转误报成某个 commit 的数值回归
|
| 73 |
+
(B 对 ba5b766 scatter-add 的 bisect 即此假信号;scatter vs one-hot 实测梯度逐位一致,
|
| 74 |
+
复现脚本 tools/debug/)。此前文档写的「共卡 cudnn 漂移、偶发 ~4%」是同一现象的
|
| 75 |
+
不完整解释。对策:梯度验收 = GPU 松验(rel_l2<5e-2,兜断链级 bug)+ CPU 紧验
|
| 76 |
+
(rel_l2<1e-4);训练不受影响(TF32 卷积训练是业界默认,torch 侧同样如此)。
|
| 77 |
+
|
| 78 |
+
## 工程
|
| 79 |
+
|
| 80 |
+
- Jittor 首跑 JIT 编译数分钟属正常;变长 shape 反复触发重编译(R5)——M5 实测后决定 bucket。
|
| 81 |
+
- 惰性求值:报错位置 ≠ 出错算子位置,二分法用 `.sync()`/`jt.flags.lazy_execution=0` 定位。
|
| 82 |
+
- 权重转换:torch `state_dict` → `{k: numpy}` pickle → `model.load_parameters`(TED 已验证)。
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
# v3 专项附录(Agent B)
|
| 87 |
+
|
| 88 |
+
# Porting Notes(torch → Jittor)— 随移植进度持续更新
|
| 89 |
+
|
| 90 |
+
## 2026-07-26 静态盘点(v3 核心四文件的 API 风险清单)
|
| 91 |
+
|
| 92 |
+
统计对象:`point2rbox_v3_head.py`(H) / `point2rbox_v3.py`(D) /
|
| 93 |
+
`point2rbox_v2_loss.py`(L, A 所有权) / `losses/utils.py`(U)。
|
| 94 |
+
|
| 95 |
+
### 🔴 Jittor 无对应,必须自实现
|
| 96 |
+
|
| 97 |
+
| API | 出现 | 对策 |
|
| 98 |
+
|---|---|---|
|
| 99 |
+
| `Tensor.index_reduce_(0, idx, src, 'amin'/'mean', include_self=False)` | H×9(实例聚合核心) | **已解**:底座 `h2rbox_v2p_head.py` L401-429 范式 = `np.unique`(CPU) 取 idx + Jittor 原生 `reindex_reduce('min'/'max'/'add')`(可导);mean = add / count。照抄该范式即可,L1 测试仍要锁语义(include_self=False 等价性、空 segment) |
|
| 100 |
+
| `torch.linalg.eigh` | D×1, L×5 | 全部 2×2 对称阵,闭式解(A 在 `ops/linalg2x2.py` 做,我验 `w==h` 退化) |
|
| 101 |
+
| `torch.linalg.solve` | D×1, L×2 | 2×2 伴随矩阵求逆,det clamp eps |
|
| 102 |
+
| `torch.diag_embed` | D×2, H×1, L×5 | jt.stack 手拼 2×2 或展开为标量表达式 |
|
| 103 |
+
|
| 104 |
+
### 🟡 语义差异需 L1 锁死
|
| 105 |
+
|
| 106 |
+
| API | 出现 | 风险点 |
|
| 107 |
+
|---|---|---|
|
| 108 |
+
| `torch.unique(return_inverse=True, return_counts=True)` | H×2 | jt.unique 的返回值/排序语义需实测;bid 聚合正确性完全依赖它 |
|
| 109 |
+
| `grid_sample` / `affine_grid` | D×4 / D×2 | rotate_crop 用;align_corners 显式传;padding_mode='reflection' 行为需比对 |
|
| 110 |
+
| `torch.gather(x, 1, idx)` | H×1 | jt.gather 语义核对 |
|
| 111 |
+
| `topk` | H×2, L×7 | 排序稳定性、返回顺序 |
|
| 112 |
+
| `F.interpolate bicubic` | D(voronoi val 上采样是 bilinear+align_corners=True) | align_corners 显式传 |
|
| 113 |
+
| `cv2.watershed` / `cv2.medianBlur` | D, L | CPU numpy 路径,原样保留,确认梯度切断 |
|
| 114 |
+
|
| 115 |
+
### 🔴 in-place 就地赋值(R5:前向对、梯度错)
|
| 116 |
+
|
| 117 |
+
- H×4、D×4、L×1 处布尔掩码赋值,重点:
|
| 118 |
+
- `pos_decoded_angle_preds[square_mask] = 0`(H L303,**在梯度路径上**)
|
| 119 |
+
- `pos_rbox_targets[~pos_syn_mask, 2:] = pos_rbox_preds[...].detach()`(H L317,右侧已 detach,但左侧张量后续参与 loss_bbox)
|
| 120 |
+
- `data_sample.gt_instances.bboxes.tensor[mask] = results.bboxes.tensor`(D L417)
|
| 121 |
+
- 对策:loss 路径一律改写为 `jt.where` / out-of-place 重组;L2 测试必须比梯度。
|
| 122 |
+
|
| 123 |
+
### 🎲 随机性调用点(测试对齐时替换为固定序列)
|
| 124 |
+
|
| 125 |
+
- D:`torch.rand`×5(dual-stream 分支选择、rot 角、scale 因子)+ `np.random.randint`(copy-paste 位置)
|
| 126 |
+
- H:`torch.rand`×1
|
| 127 |
+
- 测试策略:从 PyTorch dump 随机序列 npz,两边读同一序列(仅测试,训练不改)。
|
| 128 |
+
|
| 129 |
+
### 来自 A 的 core 层结论(2026-07-26,M5' 必须遵守)
|
| 130 |
+
|
| 131 |
+
1. **EdgeLoss 的 RoIAlignRotated**:必须用
|
| 132 |
+
`ROIAlignRotated(49, spatial_scale, sampling_ratio, aligned=True, clockwise=True)`
|
| 133 |
+
(A 在 commit 312b3b7 加的 mmcv 语义参数;**默认参数是底座旧行为,禁止用默认值**)。
|
| 134 |
+
实测 JDet 原生 kernel 方向 = mmcv clockwise=True;out_size 7/49 与 mmcv rtol 1e-4 全过。
|
| 135 |
+
残留差异:aligned=True 时 mmcv 不做 1×1 最小 roi 钳制、JDet kernel 始终钳制(亚像素 roi 才受影响)。
|
| 136 |
+
2. **⚠️ nms_rotated 返回顺序**:jdet 的 keep 是**原始顺序**,mmcv 是**按分数降序**。
|
| 137 |
+
→ v3 head 的 `_predict_by_feat_single`(test_cfg: nms 后 `max_per_img=2000` 截断)
|
| 138 |
+
**必须先按 score 降序 sort 再截断**,否则截掉的是错误的框,mAP 静默劣化。
|
| 139 |
+
3. **LR 语义**:用 A 的 `LinearWarmupMultiStepLR`(与 mmengine 1440 点逐点相等);
|
| 140 |
+
注意 mmengine LinearLR 分母是 `end-begin-1`(第 499 iter 到顶),且 A 已把
|
| 141 |
+
scheduler.step 挪到 optimizer.step 之前(f(i) 对齐)。自己写任何调度相关代码别按 torch 直觉来。
|
| 142 |
+
4. GDLoss(A 修了 3 处底座语义差异)/ PSCCoder / box_iou_rotated 均已 parity 过,直接复用。
|
| 143 |
+
5. **core-stable(13:29)随附三条**:(a) jt 的 `.max()`/`.sum()` reduce 出 shape [1] 非标量,
|
| 144 |
+
`jt.stack` 两个会得 (2,1)——聚合标量用 `jt.concat`;(b) jittor 1.3.8.5 无
|
| 145 |
+
`meshgrid(indexing=...)`,手工 `x[None,:].expand(...)` 构造;(c) w==h 简并时 eigh
|
| 146 |
+
逐特征值梯度是子梯度(基底相关,与 torch 不逐位一致但 trace 一致)——parity 测试对
|
| 147 |
+
简并用例只比不变量(trace/行列式/loss 值),不比逐元素梯度。
|
| 148 |
+
6. `segment_anything()` 已在 A 的 loss 里 lazy import `jdet.models.sam`(我的签名)✓。
|
| 149 |
+
`RotatedSingleRoIExtractor` 落在 `models/roi_extractors/rotated_single_level.py`
|
| 150 |
+
(兼容 mmcv 旧参名 out_size/sample_num)。
|
| 151 |
+
7. targets/bids 的 JDet 侧约定:待 A 移植 v2 detector(其 M4)时定,COORD 通知后照抄。
|
| 152 |
+
|
| 153 |
+
### 已确认的良性点
|
| 154 |
+
|
| 155 |
+
- head 只 build `PSCCoder` + `DistanceAnglePointCoder`,无 assigner(铁律三已证实)
|
| 156 |
+
- MobileSAM 无 `nn.MultiheadAttention`(自定义 Attention,q/k/v 独立 Linear)→ 无 in_proj 拆包
|
| 157 |
+
- MobileSAM 的 timm 依赖仅 DropPath/to_2tuple/trunc_normal_,推理期均可平替
|
| 158 |
+
- TED 纯卷积,无自定义算子;`PixelShuffle(1)` 恒等(staging 已做防御性回退)
|
| 159 |
+
|
| 160 |
+
### epoch 开关清单(SetEpochInfoHook 注入,L2 测试点)
|
| 161 |
+
|
| 162 |
+
- `copy_paste_start_epoch=6`(D)、`edge_loss_start_epoch=6`(H L414)、
|
| 163 |
+
`joint_angle_start_epoch=1`(H L298:之前 angle detach)、
|
| 164 |
+
`label_assign_pseudo_label_switch_eopch=6`(D L403:切换 predict vs generate_pseudo_targets)
|
| 165 |
+
|
| 166 |
+
## ⚠️ 勘误(2026-07-26 16:40,来自 A 的实战 + B 审计确认)
|
| 167 |
+
|
| 168 |
+
**PLAN §8 的『`.detach()` → `.stop_grad()`』建议是错的。** jittor 的 `.stop_grad()`
|
| 169 |
+
是**就地打标记并返回自身**(非 torch detach 语义):把还要参与 loss 的变量本体标成
|
| 170 |
+
无梯度 → 前向全对、部分 loss 梯度静默变 0。要 detach 语义用 `.detach()`(返回新变量)
|
| 171 |
+
或 `.clone().stop_grad()`。仅对确定无其他梯度消费者的节点(转 numpy 前、buffer、
|
| 172 |
+
新建切片节点)可直接 stop_grad。B 侧 21 处使用已逐一审计安全(loss 梯度 parity
|
| 173 |
+
1.1e-5 佐证);A 侧修复 10 处误用(commit 1aa8ad8)。
|