Tập IV trong Bộ Lò Luyện · Nguồn số tự động từ data/*.json

Lò Luyện Giải Phẫu

Sách giáo khoa dạy bạn công thức khi mọi thứ phẳng phiu. Quyển này mổ trực tiếp các file JSON trọng số, config kiến trúc và log benchmark trích tự động từ hai dự án thực tế (stv-lite v3 và ltp-ft) — nơi có trọng số đặt tay tồn tại qua hai đời kiến trúc, những bản ship không hề train lại, và những bảng số WebGPU dễ khiến kỹ sư kết luận ngược.

93 artifact Term Verifier (v1→v23) 7 kiến trúc NER (LTP Electra & Dilated-TCN) 3 bộ benchmark WebGPU (RTX 5070 Ti & Intel Xe-LPG)

Module 01 · Khảo cổ học Trọng số

01 Phả hệ 93 Artifact: Phân biệt “Train lại thật” với “Vá chính sách”

Mục tiêu

  • Hiểu vì sao nhìn tên file v1..v10 và đọc training.devMetrics trong file model có thể đánh lừa bạn hoàn toàn.
  • Dùng hàm băm trọng số đã học (learned weights hash) để phát hiện 10 bản ship thực chất chỉ có 4 lần chạy tối ưu hóa (training run) thật.
  • Hiểu tại sao cấm tuyệt đối so sánh F1 giữa hai bản có ngưỡng quyết định (threshold) khác nhau (0.50 vs 0.98).

Trong dự án stv-lite v3, bộ Term Verifier đứng sau bộ dò thuật ngữ bằng luật (rule-based detector): detector đề xuất một ứng viên (như 叶凡 hay 方才), còn verifier tính xác suất $p = \sigma(z)$ để ra một trong ba quyết định:

  • Giữ tự động (Keep): nếu $p \ge \tau_{\text{keep}}$.
  • Đẩy cho người duyệt (Review): nếu $\tau_{\text{review}} \le p < \tau_{\text{keep}}$.
  • Bỏ tự động (Reject): nếu $p < \tau_{\text{review}}$ hoặc dính luật chặn cứng (rejectGuards).

1. Phát hiện số 1: 10 bản Logistic Regression chỉ có 4 lần train thật

Khi script tools/extract/term_verifier.py quét toàn bộ 16 bản đã ship và 77 bản nghiên cứu (tổng cộng 93 artifact, lên tới v23), nó tách riêng các trọng số thực sự học từ Gradient Descent khỏi các trọng số nguyên được chèn bằng tay ($\lvert w_i \rvert \ge 5, w_i \in \mathbb{Z}$) rồi so khớp từng phần tử với độ chính xác $10^{-9}$:

NHÁNH LOGISTIC REGRESSION (v1 – v10): CHỈ 4 LẦN TRAIN THẬT v1 (Run 1) v2 (Run 2) v3 (Run 3) v4 (Run 4 · bị bỏ) v5 → v6 → v7 → v8 → v9 → v10 Dùng lại 100%47 trọng số đã học của v3! v11 → v15 (GBDT) 156–281 cây · 97–108 features
Bản đồ khảo cổ thực tế từ data/term-verifier-lineage.json. Nhánh v4 có train lại trên 4.051 dòng nhưng bị bỏ; từ v5 đến v10 đều quay về vector 47 trọng số đã học của v3, chỉ nối thêm 1–2 trọng số đặt tay và sửa rejectGuards.

Hai cái bẫy đọc log kinh điển

  1. Hóa thạch training.devMetrics: Trong file JSON của v5 đến v10, khối devMetrics vẫn ghi precision = 0.9111, recall = 0.1839, f1 = 0.3060. Con số này thừa kế nguyên xi từ v3 khi copy file và không hề phản ánh tác động của trọng số đặt tay isKnownCommonTerm = -14 / -17 hay các rejectGuards mới!
  2. So sánh F1 lệch ngưỡng: Nếu bạn nhìn v1 (F1 = 0.758) rồi nhìn v2 (F1 = 0.474) hay v3 (F1 = 0.306) và kết luận “model ngày càng tệ đi”, bạn đã sập bẫy! Ở v1, F1 được đo tại ngưỡng keep = 0.50; từ v2 trở đi, ngưỡng tự động giữ bị đẩy lên keep = 0.98 (logit $\approx +3.89$) để ưu tiên Precision tuyệt đối (>91%), đẩy phần ở giữa sang hàng đợi review. Khi sang v11+ (GBDT), dự án đổi hẳn sang đo ROC-AUC và PR-AUC (thước đo độc lập ngưỡng).

🧪 Phòng thí nghiệm 01 · Máy Quét Phả Hệ 16 Bản Ship (v1 → v15 GBDT)

Dữ liệu thật 100%

Bấm chọn chế độ lọc hoặc nhấp vào từng phiên bản trong bảng để mổ xẻ sự thay đổi giữa Model học được và Chính sách đặt tay:

Bản Tên artifact Kiến trúc Features Ngưỡng Keep / Review Trạng thái trọng số Metric ghi nhận

Chi tiết phiên bản đang chọn

Nhấp vào một dòng trong bảng trên để xem phân tích giải phẫu của phiên bản đó.

Module 02 · Phẫu thuật Scorer Production

02 Trọng số Đặt tay (Hand-set Veto) & Cuộc Di Cư Sang GBDT

Mục tiêu

  • Hiểu cơ chế Hybrid ML + Rule Veto: tại sao một trọng số đặt tay $w = -17$ trên không gian logit tương đương với một phiếu phủ quyết mềm (soft veto).
  • Thấy tận mắt cách hai luật đặt tay (isKnownCommonTerm = -17, contextModifierBeforeCount = -8) sống sót qua cuộc đại phẫu từ Hồi quy Logistic sang GBDT dưới tên gọi mới: logitOffsets.

Trong Hồi quy Logistic, xác suất được tính từ tổng tuyến tính của các đặc trưng:

$$z = w_0 + \sum_{i=1}^d w_i x_i, \qquad p = \sigma(z) = \frac{1}{1 + e^{-z}}$$

Để đạt ngưỡng keepThreshold = 0.98, ứng viên cần đạt điểm logit tối thiểu:

$$z_{\text{keep}} = \ln\left(\frac{0.98}{1 - 0.98}\right) = \ln(49) \approx +3.8918$$

Khi phát hiện các từ tiếng Trung thông dụng (như 方才 - vừa nãy) bị bộ dò thống kê nhầm thành tên nhân vật vì đứng đầu câu, thay vì gom thêm hàng nghìn mẫu và train lại toàn bộ model (dễ làm xáo trộn ranh giới của các tên người thật), tác giả đã chèn thẳng một đặc trưng nhị phân isKnownCommonTerm với trọng số đặt tay $w = -14$ (ở v5–v7) rồi tăng lên $w = -17$ (từ v8 trở đi).

Hãy tính nhẩm: dù toàn bộ 47 đặc trưng học được có cộng dồn cho từ đó một điểm logit rất cao là $+5.0$, chỉ cần isKnownCommonTerm = 1, tổng logit lập tức bị kéo tụt xuống $5.0 - 17.0 = -12.0$, ứng với xác suất $\sigma(-12) \approx 6.1 \times 10^{-6} \ll \tau_{\text{review}}$, đẩy thẳng ứng viên vào thùng Reject!

Khi chuyển sang GBDT (v11–v23): logitOffsets ra đời

Hồi quy tuyến tính không tự bắt được các tương tác bậc cao (ví dụ: một chuỗi 4 chữ có hậu tố công pháp 诀 chỉ đáng tin khi không bị kẹp giữa các hư từ). Từ v11, hệ thống chuyển sang Gradient Boosted Decision Trees (GBDT) với 156–281 cây quyết định và hơn 100 đặc trưng. Nhưng cây quyết định học từ dữ liệu có thể không phạt đủ nặng một từ nằm trong danh sách từ thường nếu từ đó xuất hiện tới 200 lần trong truyện. Giải pháp kỹ thuật cực kỳ thực dụng trong production:

$$z_{\text{final}}(x) = \underbrace{\sum_{t=1}^T \text{tree}_t(x)}_{\text{GBDT học từ dữ liệu}} + \underbrace{\sum_{k \in \text{Offsets}} \Delta_k \cdot x_k}_{\text{logitOffsets đặt tay}}$$

Hai trọng số isKnownCommonTerm: -17 và contextModifierBeforeCount: -8 từ thời Logistic Regression bước nguyên vẹn sang kỷ nguyên GBDT mà không đổi một con số nào!

🧪 Phòng thí nghiệm 02 · Phòng Mổ Trực Tiếp Scorer term-verifier.js (v1–v10 + Mô phỏng Offset v14)

Production Scorer

— —
Đặc trưng kích hoạt ($\ne 0$) Giá trị $x_i$ Trọng số $w_i$ Đóng góp $w_i x_i$

💡 Thử bấm preset “方才 · từ thường (bẫy)” rồi chuyển qua lại giữa v3 (chưa có trọng số đặt tay) và v8 (có isKnownCommonTerm = -17) để thấy cú phủ quyết trên trục logit!

Tự kiểm tra Module 01–02

1 · Tại sao từ v5 đến v10 trọng số đã học hoàn toàn giống v3 nhưng kết quả chấm ứng viên “方才” lại đảo ngược từ Review sang Reject?

Bởi vì tác giả không train lại47 trọng số cũ mà nối thêm đặc trưng nhị phân isKnownCommonTerm với trọng số đặt tay $-14$ (v5–v7) và $-17$ (v8–v10). Chỉ một đặc trưng này kích hoạt đã trừ thẳng $17$ điểm khỏi logit, kéo xác suất xuống sát $0$.

2 · Với ngưỡng keepThreshold = 0.98 và reviewThreshold = 0.01, khoảng điểm logit nào sẽ đẩy ứng viên vào hàng đợi chờ người duyệt (Review)?

Ta lấy hàm logit $\ln(p/(1-p))$: tại $p = 0.01$, $z = \ln(0.01/0.99) \approx -4.595$; tại $p = 0.98$, $z = \ln(0.98/0.02) \approx +3.892$. Vậy mọi ứng viên có điểm logit nằm trong khoảng $[-4.595, +3.892)$ đều được đưa vào diện review.

Module 03 · Giải phẫu Tham số Encoder Đa nhiệm

03 Factorized Embedding & Cái Giá Của Các Đầu Task Trong LTP

Mục tiêu

  • Tính chính xác số tham số của kiến trúc kiểu ELECTRA/ALBERT dùng Factorized Embedding ($E \ne H$).
  • Hiểu vì sao trong một mô hình NLP đa nhiệm (CWS, POS, NER, SRL, DEP, SDP), các đầu phân tích cú pháp/ngữ nghĩa có thể nặng gần bằng cả phần thân Transformer!

Khi giải phẫu file config.json của LTP tiny, ta thấy vocab_size = 21128 và hidden_size = 256. Nếu nhân theo thói quen của BERT chuẩn ($V \times H = 21\,128 \times 256 = 5\,408\,768$), bạn sẽ ước lượng sai gấp đôi! Lý do: LTP dùng kiến trúc ELECTRA với bảng nhúng phân tách (Factorized Embedding) embedding_size = 128:

$$\text{Params}_{\text{emb}} = \underbrace{(V + P_{\max} + T) \times E}_{\text{Bảng tra chiều thấp } E=128} + \underbrace{E \times H}_{\text{Ma trận chiếu } 128 \to 256}$$

Thay số thật của LTP tiny ($V=21\,128, P_{\max}=512, T=2, E=128, H=256$):

  • Bảng nhúng: $(21\,128 + 512 + 2) \times 128 = 2\,770\,176$ tham số.
  • Ma trận chiếu lên không gian ẩn: $128 \times 256 = 32\,768$ tham số.
  • Tổng khối nhúng chỉ còn $2.80\text{M}$ thay vì $5.54\text{M}$ (tiết kiệm gần $50\%$ dung lượng vocab!).

Cân nặng thật của 6 cái “Đầu” (Task Heads)

Một ngộ nhận phổ biến là “task head chỉ là một lớp Linear mỏng dính ở cuối”. Điều đó chỉ đúng với cws (tách từ, 2 nhãn: $256 \times 2 = 512$ tham số) và pos (từ loại, 27 nhãn: $256 \times 27 = 6\,912$ tham số). Ngược lại:

  • Đầu ner (RelTransformerTokenClassifier): Cắm thêm 2 tầng Relative Transformer riêng ($H=256$, 4 heads) trước khi phân loại 13 nhãn — ngốn thêm $1.58\text{M}$ tham số ở bản tiny và $14.17\text{M}$ tham số ở bản base!
  • Đầu srl (BiaffineTokenClassifier): Dùng tích song tuyến tính giữa mọi cặp vị trí vị ngữ – đối số với tensor bậc ba kích thước $L \times (d + 1) \times (d + 1)$ ($L = 97$ nhãn ngữ nghĩa). Ở LTP base1 ($d=600$), riêng cái đầu srl này ngốn $35.96\text{M}$ tham số!

🧪 Phòng thí nghiệm 03 · Máy Giải Phẫu Tham Số 7 Kiến Trúc NER Thực Tế

Trích từ config.json

Chọn một trong 7 model từ manifest data/ner-architectures.json để mổ xẻ tỷ trọng giữa Embedding, Thân mạng (Backbone) và Các đầu Task (Heads):

Module 04 · Kiến trúc Cực Nhẹ Cho Trình Duyệt

04 Dilated TCN + Span Head (Branch-C) & Bài Toán Lọc 265 Triệu Span

Mục tiêu

  • Tính chính xác Trường tiếp nhận (Receptive Field) của mạng Tích chập Giãn nở (Dilated TCN) qua công thức $R = 1 + 2 \sum d_\ell \frac{k-1}{2}$.
  • Hiểu cơ chế Span Classification (gán nhãn cho đoạn độ dài $2..6$ ký tự) và cách Role-based Span Sampling tránh chết chìm trong 265,7 triệu span âm tính.

Để chạy NER trực tiếp trong trình duyệt với ngân sách ~1 triệu tham số và tốc độ trên 7.000–9.500 câu/giây, nhánh Branch-C (branch_c_tiny_span_h128_*) bỏ hoàn toàn Self-Attention bậc hai $O(T^2)$ và thay bằng 8 tầng Tích chập 1D Giãn nở (Dilated TCN) với kích thước nhân $k = 3$, chiều ẩn $H = 128$, và lịch giãn nở (dilation schedule):

$$d = [1, 2, 4, 8, 16, 32, 1, 1]$$

Ở mỗi tầng có hệ số giãn $d_\ell$ và nhân $k = 3$ (bán kính $\frac{k-1}{2} = 1$), tầm nhìn mở rộng sang mỗi bên đúng $d_\ell$ ký tự. Sau $L$ tầng chồng lên nhau, trường tiếp nhận (Receptive Field) tính bằng số ký tự là:

$$R = 1 + 2 \sum_{\ell=1}^L d_\ell \cdot \frac{k-1}{2} = 1 + 2 \times (1 + 2 + 4 + 8 + 16 + 32 + 1 + 1) \times 1 = 1 + 2 \times 65 = 131\text{ ký tự}$$

Chỉ với $393\,216$ tham số tích chập (nhẹ hơn cả một tầng BERT!), mỗi ký tự đã nhìn bao quát 131 ký tự xung quanh — dư sức bao trọn một câu văn web novel tiếng Trung.

Tại sao Span Sampling là chìa khóa sống còn khi train Span-based NER?

Thay vì gán nhãn BIO cho từng chữ, Branch-C xét mọi đoạn con có độ dài từ min_span = 2 đến max_span = 6 và phân loại vào 4 nhãn: none, Nh (tên người), Ns (địa danh), Ni (tổ chức). Trên 129.203 câu huấn luyện, tổng số đoạn ứng viên được liệt kê lên tới 265.720.290 span, nhưng chỉ có 1.288.620 span dương tính thật ($0.485\%$)!

Nếu đưa cả 265,7 triệu span vào hàm loss, mô hình vừa tốn gấp 5 lần thời gian train vừa bị ngập trong các đoạn rác dễ đoán. Báo cáo huấn luyện trong data/ner-architectures.json cho thấy chiến lược lấy mẫu phân tầng 4 vai trò (Role-based Span Sampling) giữ lại đúng 56.874.420 span ($21.4\%$):

  • Positive ($1.29\text{M}$): giữ $100\%$ mọi thực thể thật.
  • Boundary ($30.00\text{M}$): các đoạn lệch 1 ký tự ở biên trái/phải của thực thể thật — buộc mô hình học ranh giới sắc nét, không ăn lẹm hư từ liền kề.
  • Hard Negative ($12.68\text{M}$): các đoạn không phải thực thể nhưng có điểm dự đoán cao (dễ nhầm).
  • Random Negative ($12.90\text{M}$): mẫu nền ngẫu nhiên để giữ hiệu chuẩn xác suất toàn cục.

🧪 Phòng thí nghiệm 04 · Thiết Kế Lịch Giãn Nở (Dilation Schedule) & Cổng Kiểm Duyệt preExportGate

Tương tác Toán học

Module 05 · Đo lường Phần cứng & Trình duyệt

05 Giải phẫu WebGPU NER Bakeoff: Ba Cái Bẫy Khi Đọc Bảng Số Tốc Độ

Mục tiêu

  • Phân biệt rõ Kiến trúc mô hình (Model), Bộ thực thi (Runtime/Engine), Đường chạy phần cứng (Execution Provider) và Đặc tính GPU (Discrete vs Integrated).
  • Giải thích tại sao cùng một cặp model trên cùng một runtime có tỷ lệ độ trễ 1.2× trên RTX 5070 Ti nhưng vọt lên 4.8× trên Intel Xe-LPG.
  • Hiểu vì sao model lượng tử hóa dynamic_q8 lại chạy chậm hơn 4–7.6 lần so với fp16 trên đường chạy WebGPU hiện tại.

Manifest data/ner-bakeoff.json trích xuất ba bộ benchmark chạy thực tế trên trình duyệt Chrome với 3 hệ thống NER: ltp_tinyD2b (11.02 MB), uer_tiny (6.10 MB), và minirbt_h256 (19.77 MB). Nếu một kỹ sư chỉ nhìn cột batchMsMedian rồi vội vàng vẽ biểu đồ kết luận “model nào nhanh nhất” hoặc “int8 làm chậm WebGPU”, họ sẽ mắc ngay ba sai lầm phương pháp luận nghiêm trọng mà chính tác giả đã cảnh báo trong trường caveats[]:

🧪 Phòng thí nghiệm 05 · Bàn Cân WebGPU Bakeoff & Bộ Kiểm Định Kết Luận Khoa Học

Dữ liệu từ ner-bakeoff.json
Hệ thống Engine / Runtime Kích thước Độ chuẩn xác (Exact Sentences) Single Steady (ms) Batch 32 câu (ms) Thông lượng (tok/s)

Phân tích Phương pháp luận cho Lượt chạy này

Giải mã 3 hiện tượng kỹ thuật đằng sau bảng số

  1. Hiện tượng 1 — Khoảng cách giãn gấp 4 lần khi đổi GPU (1.2× → 4.8×): uer_tiny (6.1 MB) và minirbt_h256 (19.77 MB) chạy trên cùng một runtime (onnxruntime-web 1.27.0 WebGPU fp16), nên phép so sánh giữa hai hệ này là hoàn toàn hợp lệ. Ở chế độ Batch 32 câu (996 token), trên card rời RTX 5070 Ti Blackwell, độ trễ là 5.1 ms so với 6.2 ms (chênh 1.22×). Nhưng khi đem nguyên xi sang GPU tích hợp Intel Xe-LPG, uer_tiny mất 8.6 ms còn minirbt_h256 vọt lên 41.1 ms (chênh 4.78×)! Nguyên nhân: ở batch 32, lượng trọng số nearly 20 MB cùng tensor kích hoạt trung gian của minirbt_h256 vượt ngưỡng bộ nhớ đệm (cache) trên iGPU dùng RAM hệ thống băng thông thấp, gây nghẽn băng thông bộ nhớ (memory-bandwidth bound). Bài học: Benchmark trên máy trạm GPU rời không bao giờ được ngoại suy tuyến tính sang máy người dùng cuối chạy iGPU.
  2. Hiện tượng 2 — Trộn lẫn Engine với Kiến trúc (Confounding Variable): ltp_tinyD2b chạy trên bộ shader custom WGSL f16 tự viết, trong khi hai hệ kia chạy trên onnxruntime-web. Khi hai hệ thống khác nhau đồng thời cả kiến trúc mô hình lẫn trình thực thi kernel, mọi phép so sánh trực tiếp đều bị nhiễu trộn (confounded).
  3. Hiện tượng 3 — Nghịch lý dynamic_q8 chậm gấp 7.6 lần fp16: Trong lượt chạy thứ 3 trên RTX 5070 Ti, minirbt_h256_fp16 chạy mất 7.0 ms, nhưng bản lượng tử hóa minirbt_h256_dynamic_q8 lại mất tới 53.55 ms (chậm hơn 7.6 lần!). Đó không phải lỗi của toán học int8, mà vì trong phiên bản onnxruntime-web thử nghiệm đó, WebGPU Execution Provider chưa hỗ trợ đầy đủ các toán tử lượng tử hóa động (như DynamicQuantizeLinear / MatMulInteger), khiến đồ thị bị cắt vụn: các node lượng tử bị đẩy ngược về CPU/WASM xử lý rồi copy tensor qua lại giữa RAM và VRAM ở mỗi tầng!

Module 06 · Tổng kết & Nguyên tắc Thực chiến

06 Năm Quy Tắc Vàng Khi Phẫu Thuật Model & Đọc Số Liệu Production

#Quy tắc Giải phẫuBiểu hiện thực tế trong Dự ánCách kiểm tra bằng Code/Script
1 Tin vào Hash trọng số, đừng tin vào tên Version term-verifier-v5..v10 mang 6 tên khác nhau nhưng dùng chung 100% vector trọng số đã học của v3. Băm (MD5/SHA256) mảng trọng số đã lọc bỏ các luật đặt tay; kiểm tra max_abs_diff < 1e-9.
2 Không bao giờ so F1 khi đổi Threshold v1 đo ở $\tau=0.50$, v2+ đo ở $\tau=0.98$. So F1 trực tiếp là vô nghĩa. Luôn ghi kèm cặp $(\tau_{\text{keep}}, \tau_{\text{review}})$ hoặc dùng diện tích dưới đường cong PR-AUC / ROC-AUC.
3 Tách bạch Luật phủ quyết (Veto) khỏi Trọng số học isKnownCommonTerm = -17 bảo vệ hệ thống tức thì trước lỗi false-positive rõ ràng mà không cần retrain. Đóng gói luật cứng thành khối logitOffsets và rejectGuards tường minh thay vì trộn ngầm vào mảng trọng số.
4 Đếm tham số phải nhìn cả Vocab Projection lẫn Task Heads ELECTRA giảm nửa bảng vocab nhờ $E=128 \to H=256$, nhưng đầu srl Biaffine và ner RelTransformer nặng hàng chục triệu param. Viết hàm kiểm toán tham số theo từng khối (embedding, projection, backbone, heads) và đối chiếu với deployable.
5 Mọi con số tốc độ phải ghi rõ (Model, Runtime, EP, GPU, Batch) dynamic_q8 chậm gấp 7.6× vì rớt EP về WASM; khoảng cách 2 model giãn gấp 4× khi đổi từ RTX 5070 Ti sang Intel Xe-LPG. Bắt buộc log executionProvider, adapter.architecture, và tách riêng firstPassMs (shader compile) với steadyMs.

Tự kiểm tra Module 03–06

1 · Nếu giữ nguyên 8 tầng tích chập có lịch giãn nở [1, 2, 4, 8, 16, 32, 1, 1] nhưng đổi kích thước nhân từ k = 3 lên k = 5, trường tiếp nhận (Receptive Field) sẽ là bao nhiêu ký tự?

Với $k = 5$, bán kính nhân mỗi bên là $(k-1)/2 = 2$. Tổng các hệ số giãn nở là $\sum d_\ell = 65$. Vậy $R = 1 + 2 \times 65 \times 2 = 261$ ký tự (tăng từ 131 lên 261 ký tự).

2 · Tại sao khi xuất model NER ra chạy trình duyệt, số tham số deployable của Branch-C (1.142.516) lại nhỏ hơn số tham số lúc training (1.143.419)?

Chênh lệch đúng $903$ tham số nằm ở auxiliaryTokenHead (đầu phân loại phụ theo từng ký tự dùng để hỗ trợ hội tụ trong lúc train ở Stage A/B). Khi xuất bản chạy thật (inference), chỉ cần đầu phân loại Span chính nên đầu phụ $903$ tham số được cắt bỏ hoàn toàn.

3 · Trong bảng WebGPU NER Bakeoff, phép so sánh nào là hợp lệ để kết luận về chi phí kiến trúc mô hình?

Chỉ phép so sánh giữa uer_tiny và minirbt_h256 ở chế độ fp16 là hợp lệ để so sánh mô hình, vì cả hai chạy trên cùng một engine (onnxruntime-web 1.27.0), cùng một Execution Provider (WebGPU fp16), và cùng một kịch bản workload.