可維護性重構 P0–P3 完成報告¶
這是重構完成時的歷史稽核快照;branch、test count、工作機路徑與行數不代表 0.6.1 現況。當前不變條件與命令以根目錄
AGENTS.md為準。
- 基底:P0–P2 已合併;P3 基於
14f9e72 - 依據:
renewable-huber 代碼結構與可維護性稽核報告(稽核版本 df17d08) - 狀態:P0、P1、P2、P3 全部完成並驗證
0. 先讀這一節:編號會撞名¶
這份文件裡的 P0/P1/P2 是稽核報告的「維護性優先序」,和 docs/native-core-p0-baseline.md、native-core-p1.md、native-core-p2.md、native-core-p4.md 裡的 native core 實作階段完全無關。
| 編號 | 本文件的意思 | docs/native-core-*.md 的意思 |
|---|---|---|
| P0 | 修 C ABI 契約、建跨語言 contract、拆 engine.cu |
純 Python 基線 |
| P1 | Python backend capability contract | Rust CPU 引擎 |
| P2 | 拆 Rust 模組 | CUDA whole-batch 引擎 |
| P3 | persistence 邊界、測試 profile、benchmark sweep 拆分(完成) | — |
看到 "P2" 時務必先確認是哪一套。
1. 這次重構的原則¶
專案根目錄的
AGENTS.md是這份報告的精簡版,只放「違反會安靜壞掉」的不變條件 與驗證指令;改動任何一邊時請保持兩邊一致。
三條,違反其中任何一條的修改都應該被退回:
- 不改演算法、kernel 順序、stream 行為、公開 API。 這是純結構重構。重構期間的正式範圍為 CPU 1.17×–15.65×(現行 p6 基準為 1.38×–7.86×,見
AGENTS.md)、CUDA host 1.04×–1.96×、CUDA DLPack 1.06×–2.04×;golden corpus 必須逐位元保持。 - 隱性契約要變成會失敗的測試。 稽核指出的問題不是「程式碼很醜」,而是「契約漂移時沒有任何東西會壞」。每一項修正都附帶一個會抓到回歸的測試。
- 每一步都要能獨立驗證。 特別是
engine.cu的拆檔,分成八個可各自 build + 測試的步驟。
2. P0:降低立即風險¶
2.1 C ABI batch/intercept 契約(commit d51503c)¶
問題:rh_cuda.h 寫 x_design 形狀為 (n_rows, n_parameters)、intercept 由 caller 建構。但 validate_batch 早就同時接受 n_columns == n_features_in,copy_batch 在該情況於 device 端補上 intercept 欄。而 NativeCudaBackend.native_design_matrix 刻意不在 host 展開 —— 窄矩陣才是生產熱路徑,header 描述的是沒人走的路。
做法:改文件對齊實作。改實作會讓 CUDA backend 退回 host 端 column_stack,在每次 H2D 前多複製整個 batch。
同時補上 RhCudaDeviceBatch 原本完全沒有的寬度說明,並在 RhCudaUnpenalizedConfig 註明 n_parameters ∈ {n_features_in, n_features_in + 1} 的 intercept 不變條件。
順帶修掉一個真實漏洞:validate_config 原本只檢查 0 < n_features_in <= n_parameters。直接用 C ABI 傳 n_features_in=1, n_parameters=4 加窄 batch,device 端的 append 只會填 2 欄,d_design 其餘欄位是前一批的殘留資料,solver 就對著未初始化記憶體求解。Rust wrapper 的 validate_unpenalized_config 本來就擋住這個組合,C ABI 沒有。現在兩邊一致。
注意:溢位。原本想寫
n_features_in + 1 != n_parameters,但n_features_in上界不受限,INT64_MAX + 1是 UB。實作用減法engine->n_parameters - config->n_features_in比較,兩個運算元都是正數。
2.2 跨語言 contract manifest(commit 21ddf00)¶
問題:同一組契約獨立寫在四個地方 —— rh_cuda.h、Rust 手寫 mod ffi、兩個 PyO3 result-dict builder、兩份 Python _decode_result —— 而且沒有任何一處會在漂移時失敗。三個具體缺口:
- Rust 那 18 處
size_of::<ffi::X>()是把 Rust 的 layout 報告給 C,從來沒驗證過它等於 C 的 layout。 mod ffi被#[cfg(feature = "cuda")]包住,所以 CI 的cargo test -p rh-cuda-ffi根本沒編譯到那些 struct。rh_cuda_abi_version()在 header 有、engine.cu有實作,但 Rust 的 extern block 沒宣告它。version()["abi_version"]回傳的是 Rust 常數,自己對自己。
做法:native/contracts/rh_cuda_contract.json 成為單一真實來源。裡面的數字由編譯 rh_cuda.h 產生,不是手算(用一支 dump 程式印出 sizeof/offsetof)。
三份鏡像各自對照它:
| 鏡像 | 檔案 | 由誰檢查 |
|---|---|---|
| C++ | native/cuda/src/abi_contract.cpp(170 個 static_assert,不定義任何符號) |
真實編譯器。rh_cuda.h 只 include <stdint.h>,所以 g++ -fsyntax-only 就能驗,CI 免 GPU 免 CUDA Toolkit |
| Rust | native/crates/rh-cuda-ffi/src/sys.rs 的 mod abi_layout(offset_of!) |
既有的無 CUDA CI job |
| PyO3 | rh-python-cuda 的 set_item key 集合 |
Python 裁判 |
tests/test_native_cuda_contract.py 是裁判,失敗時輸出 unified diff 並指名雙方檔案。
關鍵決定:不用 bindgen。 稽核報告建議 pregenerated bindgen,但 bindgen 需要 libclang 與 CUDA headers、只重生 mod ffi(那部分本來就對)、對 status code / dtype code / flag bits / Python dict keys 零覆蓋,而 rh-cuda-ffi 必須在完全沒有 CUDA toolchain 時也能編譯。manifest + 三鏡像成本更低、覆蓋更廣。
Rust 側的關鍵重構:把 #[repr(C)] records 從 cfg-gated 的 mod ffi 移到未 gate 的 mod abi,mod ffi 只剩 pub use super::abi::*; 加 extern block。下游全部仍寫 ffi::RhCudaHostBatch,18 處 size_of 一行都沒改,而 layout 測試從此在無 CUDA 的 CI job 裡跑。
真正的 runtime 檢查:宣告 rh_cuda_abi_version(),在建 engine 前比對,不符就拒絕;version()["abi_version"] 改回報實際連結的 library。這是唯一能抓到 stale binary 的機制 —— 任何 source-level 測試都取代不了。
2.3 engine.cu 拆檔(commits 3212d1e / d100ab6 / 76668f8)¶
2,638 行單一 TU 拆成 7 個,最大 528 行。
| 檔案 | 行數 | 職責 |
|---|---|---|
c_api.cu(原 engine.cu) |
566 | guards、error translation、17 個 extern "C" |
pipeline.cu |
341 | 一次 batch transition、host prediction |
linear_solver.cu |
301 | Cholesky → LU → lazy SVD |
objective.cu |
528 | objective、gradient/Hessian、candidate CUDA Graph |
batch.cu |
138 | batch 驗證、staging、intercept append |
workspace.cu |
270 | device 配置與 engine destructor |
engine_internal.cu |
49 | memory pool registry、Failure key function |
加上 5 個 header:engine_internal.cuh、blas_traits.cuh、engine_state.cuh、以及各模組的 .cuh。
與稽核建議的四點差異(都有理由):
- 不做
memory_pool.cu—— 40 行加一個 function-local static,沒有天然同伴,併入engine_internal.cu。 - 不做
prediction.cu——predict_typed只有 50 行,且與 update 共用ensure_batch_capacity/d_design/d_residual;獨立出去反而讓d_residual的雙重角色更難看見。 - 新增
blas_traits.cuh—— 稽核清單沒有給Blas<T>/Solver<T>(316 行)任何歸屬,而 4 個新 TU 需要它。放engine_state.cuh會讓每個 consumer 被迫拉進cublas_v2.h與cusolverDn.h。 - 新增
batch.cu—— 稽核清單沒有 batch 層的家。刻意獨立,因為這正是契約漂移與測試缺口所在。
拆解順序刻意把「改 linkage」(step 1)和「改 TU 數量」(step 2–8)分開,出事時知道是哪一類造成的。
2.4 C++ smoke test 補洞¶
rh_cuda_smoke.cpp 原有 317 行完全沒動(它是拆檔的迴歸基線),新增七個案例:
- intercept append 差分測試(p=2、p=3) —— 同一 zero state 跑寬/窄矩陣,要求 1e-12 內一致。兩條路只差在
d_design怎麼填,後續 cuBLAS 看到的完全相同。一次抓到 intercept 放錯欄、row stride 錯、少元素、填錯值。 - p=3 不是多餘的:
feature_columns == 1時features[row*cols+col]與features[col*rows+row]是同一個式子。把 kernel 索引轉置驗證過:p=2 仍然通過,p=3 失敗(wide 0.166667 vs narrow 0.05)。 - device-input intercept append ——
copy_kind == DeviceToDevice時copy_batch跳過 staging,直接讀 caller 的 device pointer。這條分支原本完全沒測到。 d_yalias 還原 —— device update 後緊接一次 host update 並要求成功。ScopedPointerAlias若哪天不還原,下一次 host update 會寫進 producer 已釋放的記憶體。- device pointer 拒絕 —— 刻意不 assert 確切狀態碼:
cudaPointerGetAttributes對未註冊 host pointer 的行為隨 CUDA 版本而異(舊版 →CUDA_ERROR,11+ →INVALID_ARGUMENT),兩者都是正確拒絕。 - 跨 TU 狀態碼一致性 —— 見第 6 節。
- ABI 版本自檢。
3. P1:Python backend capability contract(commit cf8b7a8)¶
問題:ArrayBackend 宣告 9 個方法,但實際控制流另外在 13 處用 getattr 探測能力,外加一處 backend.name in {"numpy","cupy"} 字串嗅探。少掉一個能力會靜默退回 portable 路徑:結果仍正確、可能慢上數量級、而且沒有任何東西會失敗。
做法:src/renewable_huber/backends/capabilities.py 的 capabilities_of() 是唯一做探測的地方。
兩個欄位不是固定值,這是設計上最容易搞錯的地方:
effective_n_jobs在 native engine 建立前回報請求的執行緒數,建立後回報 engine 確認的數量。cuda_features帶著會累加的 CUDA Graph counter。
所以 capabilities 存的是零參數 accessor(read_n_jobs / read_cuda_features)而非快照。tests/test_backend_capabilities.py 直接針對這個差別測試 —— 若改成快照,其他所有測試都還是會過。
elementwise_workspace 取代字串嗅探。native backend 繼承自 NumPyBackend,所以必須明確 supports_elementwise_workspace = False opt out,否則會靜默加入那個集合。有測試盯著這個四分法。
共用 native adapter:backends/native_base.py 吸收 CPU/CUDA 各自寫過一遍的 ABI 協商、engine mirror token、restore 樣板,以及那個除了一行註解外逐字相同的 _decode_result。兩個行為用 hook 保住而不是壓平:CUDA 的 canonical-empty 捷徑成為 _new_engine_already_holds,各自內聯 4 處的 discard 邏輯成為 _engine_call context manager。
partial_fit 從 69 行做五件事變成 26 行:validate → prepare → transition → commit。驗證完全發生在碰任何 fitted attribute 之前,所以被拒絕的 batch 可證明不會留下半更新的 estimator。
意外的效能收穫:portable NumPy 路徑 median fit 2.94ms → 2.56ms。那三個 fused kernel 探測在求解迴圈裡每次迭代都跑,而失敗的 getattr 內部會拋出並捕捉 AttributeError;換成讀快取的 dataclass 就沒這個成本。
4. P2:拆 Rust 模組(commits b66616d / 655c12e)¶
公開 crate API 不變,用 rustdoc 快照逐項驗證。
rh-cpu:1,530 行拆成九個,最大 418。kernels 按操作對象分組:vector(batch 上的 elementwise 與 reduction)、gram(p×p 累積、決定何時啟用 Rayon)、objective(求解器實際最小化的東西)。bandwidth/lambda 是純 scalar policy,三者都不屬於,留在 kernels/mod.rs。三個 parallelism threshold 留在 lib.rs 當 pub(crate) —— workspace、engine、kernels 都讀它,放進任何一個都會倒轉依賴方向。
rh-cuda-ffi:raw C 宣告原本躺在包裝它們的 safe API 同一檔案底部。sys.rs 現在單獨持有它們(未 gate 的 records、cfg-gated extern block、layout tests),讓「這份宣告跟 rh_cuda.h 一致嗎」變成可獨立完成的 review。其餘分為 types / validation / engine / runtime。
rh-python-cuda:DLPack 移到 dlpack.rs。那是整個 extension 裡唯一解參考其他框架交過來的指標、也是唯一正確性靠協定而非 Rust 型別系統保證的地方。
效能未量前後對比,這是刻意的:Rust 的 mod 是 crate 內的命名空間,crate 才是編譯單元,所以不像 engine.cu 拆檔那樣可能影響 inlining —— 機制上不存在。仍跑了一次 CUDA host sweep 作 sanity check,八個 shape 全部落在 P0 建立的區間內。
5. 怎麼重跑每一道 gate¶
# Python(含契約裁判與 capability contract)
.venv/bin/python -m unittest discover -s tests
# lint
.venv/bin/python -m ruff check src tests scripts
.venv/bin/python -m ruff format --check src tests scripts
# Rust(layout tests 在這裡跑,不需要 CUDA)
cd native
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo check --locked --workspace --all-targets
# 注意 crate 範圍:不能用 --workspace,理由見下方
cargo test --locked -p rh-core -p rh-cpu -p rh-cuda-ffi --all-targets
# C ABI layout,不需要 GPU 也不需要 CUDA Toolkit
g++ -std=c++17 -fsyntax-only -I native/cuda/include native/cuda/src/abi_contract.cpp
cargo test在 Linux 上不能用--workspace。 PyO3 的extension-modulecrate(rh-python-cpu、rh-python-cuda)在 Linux 連成獨立測試執行檔時,CPython 的符號無法解析,cc會失敗。這不是回歸 ——ci.yml早就用限定 crate 的寫法並 註記了原因。Windows 上因為 PyO3 連結 Python import library 的方式不同而不會出現。 那兩個 crate 由cargo check/clippy(workspace 範圍)與實際的 wheel 建置涵蓋。
CUDA 部分(需要 nvcc):
cmake -S native/cuda -B build/static -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRH_CUDA_BUILD_SHARED=OFF -DRH_CUDA_BUILD_TESTS=ON -DCMAKE_CUDA_ARCHITECTURES=native
cmake --build build/static && ctest --test-dir build/static --output-on-failure
export table 不變條件(見第 6 節):
cmake -S native/cuda -B build/shared -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRH_CUDA_BUILD_SHARED=ON -DCMAKE_CUDA_ARCHITECTURES=native
cmake --build build/shared
nm -D --defined-only --extern-only build/shared/librenewable_huber_cuda.so | grep rh_cuda_
Linux 用 nm -D,Windows 用 dumpbin /exports。基線在 artifacts/baseline-exports.txt(Windows 產生,符號名相同)。
6. 不變條件清單:違反會「安靜壞掉」¶
這一節是本文件最重要的部分。以下每一項若被破壞,程式不會 crash、數值仍然正確,只有契約悄悄失效。
| # | 不變條件 | 破壞後的症狀 | 守門的東西 |
|---|---|---|---|
| 1 | 匯出恰好 17 個 rh_cuda_* 符號 |
ABI 表面擴大或縮小,下游連結行為改變 | artifacts/baseline-exports.txt 手動比對。Linux 上額外會看到幾個 libstdc++ 的 std::string template 實例化(nm 標記 W,弱符號)—— 那是 libstdc++ 標頭自帶 default visibility 所致,與本專案無關,比對時只看 rh_cuda_ 前綴 |
| 2 | Failure 不得放進 header 的 anonymous namespace,且必須保留 out-of-line destructor |
每個 TU 得到不同型別,catch (const Failure&) 停止匹配,所有錯誤狀態碼變成 INTERNAL_ERROR (8) |
smoke 的 status_survives_translation_units |
| 3 | RhCudaEngine 必須留在 global scope |
rh_cuda::RhCudaEngine 與 ::RhCudaEngine 成為兩個型別,ABI 的那個永遠 incomplete |
編譯錯誤(會 fail fast,相對安全) |
| 4 | mod abi 不得放回 #[cfg(feature = "cuda")] |
layout tests 停止在無 CUDA 的 CI 跑,Rust 鏡像靜默失守 | test_repr_c_records_are_not_gated_on_the_cuda_feature |
| 5 | manifest 先改,四個鏡像後改 | 鏡像之間互相矛盾 | tests/test_native_cuda_contract.py |
| 6 | 契約測試的每個 parser 必須先 assert「找到的數量 == manifest 預期數」 | regex 哪天靜默 match 不到,整套契約測試變成永遠通過的空殼 | assert_found()(已內建) |
| 7 | allocate<T> 有三種實例化(float/double/int,後者給 d_pivots、d_solver_info) |
若「整理」成顯式實例化清單會漏掉 <int> |
link error(安全) |
| 8 | 預設參數只屬於宣告,不得在定義重複(smooth_objective、update_typed) |
編譯錯誤(安全) | 編譯器 |
| 9 | capabilities_of 以實例快取 —— backend 建構後不得增減方法 |
快取過期,能力探測結果錯誤 | 無自動守門,靠 resolve_backend 的建構慣例 |
| 10 | read_n_jobs / read_cuda_features 必須是即時 accessor,不能改成快照 |
n_jobs_ 永遠回報建 engine 前的值;graph counter 凍結 |
tests/test_backend_capabilities.py::LiveAccessorTests |
| 11 | Rust 拆模組時用 pub(crate) 而非 pub |
公開 API 意外擴大 | rustdoc 快照比對(手動) |
| 12 | cfg-gated 項目的 import 必須帶同樣的 gate | 無 CUDA 建置解析失敗 | cargo check -p rh-cuda-ffi(不帶 feature) |
| 13 | n_parameters ∈ {n_features_in, n_features_in + 1} |
窄 batch 時 device append 只填部分欄位,solver 讀殘留記憶體 | validate_config + smoke 的 intercept_invariant_is_enforced |
6.1 rustdoc 快照的陷阱¶
比對公開 API 時,從私有模組 re-export 會讓 rustdoc 產生 redirect stub(engine/fn.predict.html 之類,declaration 與 members 皆空)。第一次比對時它們被算成「新增 8 個公開項目」。工具必須排除含 http-equiv="refresh" 的頁面,否則模組重排會被誤報成 API 變更。
6.2 效能量測的雜訊特性¶
CUDA benchmark harness 在這台機器上,同一份 binary 連跑三次 median 就會漂 2%–19%(wide float32 最差)。單次前後對比曾顯示 wide f32 +12%,實際是熱漂移。
要判定效能是否退步,必須用交錯 A/B(A/B/A/B 交替、比較配對差)而非「先跑五次 A 再跑五次 B」—— 後者比較的是兩個不同的熱狀態,不是兩份 binary。P0 用 14 對配對測量得到:f32 有 9 對為正、f64 有 8 對,與擲硬幣無異。
此 harness 在最吵的 shape 上約只能分辨 ±10%。 低於這個幅度的差異不要下結論。
7. 環境現況¶
7.1 WSL(新的主要工作環境)¶
- 位置:
/home/untrollor/renewable-huber(ext4,不是/mnt/c;後者慢且有權限與大小寫問題) - Ubuntu 24.04.3,kernel 6.6.87.2-microsoft-standard-WSL2
origin已指回https://github.com/Funtrollor/renewable-huber.gitartifacts/的 105 個驗證基線已一併帶過來(gitignored)- 已安裝並驗證:
- gcc 13.3.0、g++、make、cmake 3.28.3、ninja 1.11.1、pkg-config
- Python 3.12.3、pip、venv(
.venv/,editable 安裝指向src/) - Rust 1.97.1(rustup,
~/.cargo,含 rustfmt/clippy) - native CPU extension 已建置並安裝(
cp312manylinux wheel),abi=1 api=2 - GPU passthrough 可用:
nvidia-smi看得到 RTX 5070 Ti,driver 596.49,/usr/lib/wsl/lib/libcuda.so存在 - CUDA Toolkit 12.9 已安裝(
/usr/local/cuda),native CUDA extension 已建置並載入,abi=1 api=3,cuda_available=True
已在 WSL 通過的 gate:
| gate | 結果 |
|---|---|
python -m unittest discover -s tests |
197 passed, 43 skipped(多出的 9 個 skip 是 CUDA 測試) |
ruff check / ruff format --check |
通過 |
g++ -fsyntax-only ... abi_contract.cpp |
170 個 static_assert 成立 |
cargo fmt --all -- --check |
乾淨 |
cargo clippy --workspace --all-targets -- -D warnings |
0 diagnostics |
cargo test -p rh-core -p rh-cpu -p rh-cuda-ffi |
14 passed(含兩個 golden corpus replay 與 layout tests) |
cargo test -p rh-cuda-ffi --features cuda |
5 passed(真正編譯 extern block 的組態) |
ctest(C++ ABI smoke) |
1/1 passed |
nm -D 匯出比對 |
與 Windows 基線的 17 個符號逐字相同 |
整個 CUDA 流程已在 Linux 驗證通過。過程中修掉三個只在 Linux 才會浮現的可攜性問題,
都記在 native/crates/rh-cuda-ffi/build.rs 與 scripts/setup-wsl-venv.sh:
| 症狀 | 原因 | 修法 |
|---|---|---|
unable to find library -lcudart |
build.rs 只在 CUDA_PATH 有設時才加連結搜尋路徑。那是 Windows 安裝程式設的變數,Linux 沒有 |
依序查 CUDA_PATH/CUDA_HOME/CUDA_ROOT、從 PATH 上的 nvcc 反推 toolkit 根目錄、最後退回 /usr/local/cuda;後綴試 lib64、targets/x86_64-linux/lib、lib |
import 時 undefined symbol: __gxx_personality_v0 |
CUDA C ABI 是會拋例外的 C++,需要 libstdc++ 的例外處理常式。MSVC 透過 .lib 內嵌的預設函式庫指令自動帶入,GNU 工具鏈不會,而 rustc 傳 -nodefaultlibs |
Linux 連 stdc++、macOS 連 c++ |
maturin 要求 patchelf |
預設會把 CUDA .so 全部打包進 wheel 並改寫 rpath |
本機開發用 --auditwheel skip;系統已有 toolkit,ldconfig 會在 import 時解析。發布 wheel 由 packaging workflow 負責,不走這支腳本 |
前兩項是 build.rs 的實質 bug,不是環境設定問題 —— 那個檔案只在 Windows 上被驗證過。
這也說明為什麼把開發環境搬到 Linux 對這個專案有實質價值:CI 跑的就是 Linux。
若要在另一台機器重建:
bash scripts/setup-wsl-toolchain.sh --cuda # NVIDIA wsl-ubuntu repo,只裝 toolkit
bash scripts/setup-wsl-venv.sh --profile cuda-full
絕對不要在 WSL 裡安裝 Linux 顯示驅動。 WSL 的 GPU 是透過 Windows 驅動 passthrough,裝 Linux driver 會遮蔽掉它。上面的 repository 只含 toolkit。
7.2 Windows/PowerShell 的坑(若還會用到)¶
2>&1用在原生執行檔上會造成假失敗:PowerShell 5.1 把每行 stderr 包成 ErrorRecord,即使 exit code 是 0 也會讓$?變成$false。maturin 建置成功卻回報 exit 1 就是這個原因。導向檔案再讀取。- heredoc 不存在,
<<'EOF'是 parser error。多行字串用 here-string 或改用 Bash 工具。 - CMake generator 快取:
cmakecrate 在 Windows 預設選 Visual Studio generator,與既有的 Ninja 快取衝突。建 CUDA wheel 前必須$env:CMAKE_GENERATOR = "Ninja"(scripts/native/build_native_cuda.ps1有做,直接呼叫 maturin 時要自己設)。 Set-Content -Encoding utf8會加 BOM,Python 讀檔要用utf-8-sig。
8. P3:已實作並驗收¶
基於 14f9e72 完成,並已合併。
8.1 CheckpointPayload 邊界¶
serialization.py 變成純 codec:CheckpointPayload + write_checkpoint / read_checkpoint。它不再 import estimator、不建構 estimator、也不呼叫 _restore_state;那三件事移到 estimator 層的 _checkpoint_payload() 與 _from_checkpoint_payload()。save() / load() / state_dict() 的外部行為與 v2 檔案格式逐位元不變。
save_model / load_model 兩個模組層函式被移除 —— load_model 必然要建構 estimator,與這條邊界互斥。兩者都不在 __all__、不在任何文件裡,唯一呼叫者是 estimator.py。這是本次唯一的內部介面變更,已隨 P3 接受並記入 CHANGELOG。
diagnostics 是 payload 的欄位但沒有任何已發布格式會寫它:解 v1/v2 一律得到 None,剛 load() 的模型讀 diagnostics_ 仍拋 NotFittedError。要落檔就得升 checkpoint 格式 v3。
8.2 測試 profile¶
稽核報告寫「為 pytest 加上 markers」,前提是錯的:沒有任何測試 import pytest,CI 一律跑 python -m unittest,repo 裡也沒有 conftest.py。只加 marker 對 CI 完全無作用。
實作改成 scripts/run_test_profile.py:core / optional-cpu / native-cpu / cuda / performance / all 六個 profile,成員逐條列出並可用 --check 驗一致性(重複、遺漏、錯字、不存在的 module、未知 requirement)。前五個是 required:先探測宣告的相依或裝置,缺了就 exit 2,不會把「整套都是 skip」報成成功。python -m unittest discover -s tests 完全不受影響。
8.3 拆 benchmark_shape_sweep.py¶
1,140 行拆成 shape_sweep/ 底下五個模組(shapes / environment / timing / runners / cli),入口檔剩 100 行並 re-export 兩個 consumer 依賴的 11 個名字。schema-v2 記錄以 pre/post 對照證明結構相同:固定取樣區塊下 120 cases、52 skips、4 個 dataset checksum 一致;每個結果有 28 個欄位,其中 22 個非揮發欄位逐值相等。
9. 已知未修的小瑕疵¶
重構過程中發現、判斷不屬於本次範圍而刻意保留的:
- CUDA
state_dict()走state_dict_from_parts,不含state_is_detachedkey(CPU 版有)。目前無害,因為_decode_result用.get(..., False)保守複製。manifest 已把這個差異明文化。 _restore_state不設_diagnostics,所以剛load()的模型讀diagnostics_會拋NotFittedError。P3.1 之後這變成刻意且有測試守門的行為:checkpoint 檔案裡沒有 diagnostics,捏一份出來比拋錯更糟。見第 8.1 節。- 可攜式 core 從不設
used_regularized_fallback(永遠False),但兩個_decode_result都把它當必要 key。 _design_matrix只在傳入backend=時才委派 ——partial_fit傳、predict不傳。這是刻意的(CUDA engine 的 predict 需要完整n_parameters寬度,update 接受未展開的),已加註解說明。
10. 檔案尺寸現況¶
拆檔後仍在 500 行以上的檔案,都屬於「大但內聚」,不建議為了行數再拆:
| 檔案 | 行數 |
|---|---|
native/crates/rh-cuda-ffi/src/engine.rs |
693 |
native/crates/rh-python-cuda/src/lib.rs |
627 |
native/crates/rh-python-cpu/src/lib.rs |
615 |
src/renewable_huber/estimator.py |
591 |
native/cuda/src/c_api.cu |
566 |
native/cuda/src/objective.cu |
553 |
native/cuda/src/huber_kernels.cu |
542 |
src/renewable_huber/core/update.py |
514 |
稽核報告自己就把 core/update.py 列為「大但內聚」的正面例子。rh-python-cpu/src/lib.rs 是唯一還沒動過的 PyO3 crate,若之後要拆,可比照 rh-python-cuda 的做法。