mp-juuuns commited on
Commit
a589253
·
verified ·
1 Parent(s): 9ef9281

Add the sealed final test and expand the 65-arm field with seven figures

Browse files

New section 'The final test': the 2026-09-23 fixed-protocol evaluation of the SemEval-2020 Task 11 v2 re-split, reported in full including the two arms that fell. Prior test exposure is disclosed; this is not a never-seen holdout.

'Beside other models' gains four figures on the whole 65-arm field: the Pareto frontier on latency and energy, the metric spans (macro F1 1.2x against 64.4x/42.9x/40.3x on file/energy/latency), depth within each encoder family (7 of 12 peak below full depth), and all 65 arms with seed SDs against the all-positive floor.

README_KO.md mirrors both. SHA256SUMS regenerated: 343 -> 351 entries.

.gitattributes CHANGED
@@ -19,3 +19,10 @@ docker-train/vendor/runtime_source.tar.gz filter=lfs diff=lfs merge=lfs -text
19
  models/semeval-propaganda/tokenizer.json filter=lfs diff=lfs merge=lfs -text
20
  models/semeval-propaganda/seeds/seed42/tokenizer.json filter=lfs diff=lfs merge=lfs -text
21
  models/semeval-propaganda/seeds/seed43/tokenizer.json filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
19
  models/semeval-propaganda/tokenizer.json filter=lfs diff=lfs merge=lfs -text
20
  models/semeval-propaganda/seeds/seed42/tokenizer.json filter=lfs diff=lfs merge=lfs -text
21
  models/semeval-propaganda/seeds/seed43/tokenizer.json filter=lfs diff=lfs merge=lfs -text
22
+ benchmark/figures/11_final_test_calibration_to_test.png filter=lfs diff=lfs merge=lfs -text
23
+ benchmark/figures/12_final_test_per_label.png filter=lfs diff=lfs merge=lfs -text
24
+ benchmark/figures/13_final_test_seed_spread.png filter=lfs diff=lfs merge=lfs -text
25
+ benchmark/figures/14_field_quality_vs_cost.png filter=lfs diff=lfs merge=lfs -text
26
+ benchmark/figures/15_field_metric_spans.png filter=lfs diff=lfs merge=lfs -text
27
+ benchmark/figures/16_field_depth_effect.png filter=lfs diff=lfs merge=lfs -text
28
+ benchmark/figures/17_field_resolution_floor.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -95,6 +95,10 @@ architecture is what costs the time, not the vocabulary cut or the depth — but
95
  that, so it stays a guess. What the cut and the depth did move is the file and the energy, and
96
  those are visible in the panels above.
97
 
 
 
 
 
98
  ---
99
 
100
  ## Run it in a container
@@ -438,8 +442,120 @@ Full table: [`benchmark/full/61_arm_bf16.csv`](benchmark/full/61_arm_bf16.csv).
438
  ![the top arms overlap](benchmark/figures/02_top_arms_overlap.png)
439
  ![depth](benchmark/figures/03_depth_quality_and_cost.png)
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  ---
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  ## Two things we checked because they would have changed the reading
444
 
445
  ### Does the host matter?
@@ -637,8 +753,10 @@ described at the top of this card.
637
 
638
  ## What this does not tell you
639
 
640
- - **No held-out evaluation.** Every number here is the opened 56-document calibration split.
641
- There is no `test.jsonl` in this repository.
 
 
642
  - **The evaluation is small.** 56 documents, 14 labels, and the rarest label appears in 6 of
643
  them, so one fourteenth of macro F1 rides on six samples. This is the most likely source of the
644
  0.005–0.039 seed SDs.
@@ -661,7 +779,7 @@ described at the top of this card.
661
  | root | the 39,866-entry headless backbone |
662
  | [`root_manifest.json`](root_manifest.json) | its vocabulary rule, layer maps, hashes, why this grid point |
663
  | [`benchmark/full/`](benchmark/full/) | the 61-arm table, the grid, the same-host cuts |
664
- | [`benchmark/figures/`](benchmark/figures/) | the ten figures above |
665
  | [`runtime/`](runtime/) | the two relaxed sources of the dedicated runtime, and its own README |
666
  | [`docker/`](docker/) | a container that runs the backbone with a head on it, CPU only |
667
  | [`docker-train/`](docker-train/) | train a classifier on your own data, convert, serve |
 
95
  that, so it stays a guess. What the cut and the depth did move is the file and the energy, and
96
  those are visible in the panels above.
97
 
98
+ Everything in this table is the opened calibration split. One fixed-protocol test evaluation was
99
+ run on 2026-09-23 against a re-split this project had used before — not a never-seen holdout — and
100
+ it is reported in full, decreases included, under [**The final test**](#the-final-test).
101
+
102
  ---
103
 
104
  ## Run it in a container
 
442
  ![the top arms overlap](benchmark/figures/02_top_arms_overlap.png)
443
  ![depth](benchmark/figures/03_depth_quality_and_cost.png)
444
 
445
+ ### What the whole field looks like
446
+
447
+ ![quality against cost across all 65 arms](benchmark/figures/14_field_quality_vs_cost.png)
448
+
449
+ Both panels hold the same 65 arms; only the cost axis changes. The dashed line is the Pareto
450
+ frontier — the arms that no other arm beats on quality and cost at the same time. Seven arms hold
451
+ it on latency, eight on energy, and the RoBERTa family holds the cheap half of both. This root
452
+ sits at the top of the quality axis and pays about 2.7x the latency of `roberta-base` to be there.
453
+
454
+ ![what actually varies across the field](benchmark/figures/15_field_metric_spans.png)
455
+
456
+ Laid beside each other, the spans are the finding. Across the whole field macro F1 moves **1.2x**
457
+ — 0.4756 to 0.5781 — while the weight file moves **64.4x**, GPU energy **42.9x** and document
458
+ latency **40.3x**. The quality span is narrower than even that suggests: its bottom is exactly the
459
+ all-positive predictor, so the entire field fits inside 0.093 of usable range above a model that
460
+ answers yes to everything. Peak RSS barely moves, because it is dominated by the harness rather
461
+ than by the weights.
462
+
463
+ ![depth within each encoder family](benchmark/figures/16_field_depth_effect.png)
464
+
465
+ Every encoder family was cut to 4, 6 and 8 layers under the same protocol, and its own full-depth
466
+ checkpoint was kept beside them. **In 7 of the 12 families the best arm is not the deepest one.**
467
+ `bert-base`, `mbert` and `electra-base` peak at 4 layers, `minilm-multilingual` at 6, `mpnet-base`
468
+ and `mdeberta-v3` at 8. The five families where depth does pay — roberta, xlmr, deberta-v3,
469
+ albert, modernbert — pay for it in latency and file size. Three arms did not train at all under
470
+ the shared protocol and sit exactly on the floor.
471
+
472
+ This is an observation about this task at this resolution. It is not a claim that depth does not
473
+ matter, and it is not why this root has four layers — section 3 is.
474
+
475
+ ![all 65 arms on one axis](benchmark/figures/17_field_resolution_floor.png)
476
+
477
+ The last figure is the honest summary of the ordering. The median three-seed SD across the field
478
+ is 0.0116, and six arms sit within one such SD of the best. Several SD bars are wider than the
479
+ whole gap between the top ten arms. The ranking is reported because it was measured, not because
480
+ the measurement resolves it.
481
+
482
  ---
483
 
484
+ ---
485
+
486
+ ## The final test
487
+
488
+ On 2026-09-23 one fixed-protocol evaluation was run against the SemEval-2020 Task 11 **v2 test
489
+ re-split**, under a preregistration frozen before any test byte was read.
490
+
491
+ **It is not a never-seen holdout.** This project has used the v2 split before, and the frozen
492
+ document records prior test exposure as `true`. It is not the official leaderboard test, there is
493
+ no secure-harness attestation, and nothing in it establishes Korean quality or general
494
+ intent-classification behaviour. An earlier draft of the preregistration wrongly claimed the split
495
+ had never been opened; that draft is kept beside the corrected one rather than deleted.
496
+
497
+ What the freeze fixed in advance: the roster of eleven arms, the decision threshold for every
498
+ arm/seed/precision — each read from the arm's existing calibration report and never recomputed on
499
+ test — the aggregation rule, the metric set, and the rule that every result would be published
500
+ whatever it said. The archived payload had to match SHA-256 `ed224269dc…` before extraction. It
501
+ did. 31 of 31 jobs completed, 55 documents, 14 labels, zero failures, no training and no threshold
502
+ search. All 31 per-run metrics were then recomputed from the saved probabilities by a separate
503
+ standard-library implementation and agreed with the evaluator.
504
+
505
+ ![calibration to sealed test](benchmark/figures/11_final_test_calibration_to_test.png)
506
+
507
+ | Arm | Precision | Calibration | Test | Δ |
508
+ |---|---|---:|---:|---:|
509
+ | **task-blind 4L N=32,768 — this root's classifier** | BF16 | 0.5719 ± 0.0092 | **0.5988 ± 0.0103** | **+0.0269** |
510
+ | task-blind 4L N=32,768 | Q8_0 | 0.5849 (seed 41) | 0.5862 (seed 41) | +0.0013 |
511
+ | task-blind 4L N=65,536 | BF16 | 0.5775 ± 0.0076 | 0.5832 ± 0.0047 | +0.0057 |
512
+ | task-blind 4L N=16,384 | BF16 | 0.5787 ± 0.0021 | 0.5379 ± 0.0496 | −0.0408 |
513
+ | task-blind 4L N=8,192 | BF16 | 0.5699 ± 0.0160 | 0.5555 ± 0.0070 | −0.0144 |
514
+ | historical 4L v128k | BF16 | 0.5637 ± 0.0321 | 0.5747 ± 0.0183 | +0.0110 |
515
+ | historical 8L v248k | BF16 | 0.5686 ± 0.0212 | 0.5718 ± 0.0213 | +0.0032 |
516
+ | historical 4L v248k | BF16 | 0.5602 ± 0.0391 | 0.5707 ± 0.0081 | +0.0105 |
517
+ | historical 24L v248k | BF16 | 0.5487 ± 0.0228 | 0.5612 ± 0.0102 | +0.0125 |
518
+ | roberta-base 12L | BF16 | 0.5524 ± 0.0143 | 0.5883 ± 0.0109 | +0.0359 |
519
+ | roberta-base 6L | BF16 | 0.5427 ± 0.0139 | 0.5839 ± 0.0152 | +0.0411 |
520
+ | all-positive predictor | — | 0.4756 | **0.4866** | — |
521
+
522
+ Quantization cost 0.0013 macro F1 on test, which is the same "nothing measurable" that section 4
523
+ saw on calibration, now seen on a split the thresholds were not fitted to.
524
+
525
+ **The +0.0269 is not a result about this model.** Nine of the eleven arms moved up, the eleven-arm
526
+ mean move is +0.0085, and the two RoBERTa references moved up the *most* of all. Whatever makes
527
+ this test split score higher than the calibration split applies to the whole board, so the move is
528
+ a property of the split pair rather than of any arm on it.
529
+
530
+ Neither does any arm separate from another. This root's mean ± SD interval overlaps `roberta-base`
531
+ (0.5883 ± 0.0109), `roberta-base-6l` (0.5839 ± 0.0152) and both historical 4L arms. Where it does
532
+ clear an interval it clears N=65,536 by 0.0006 and N=16,384 by 0.0010 — gaps one to two orders of
533
+ magnitude below the spread of the design that produced them.
534
+
535
+ Two arms moved down. N=8,192 fell 0.0144, and N=16,384 fell 0.0408 while carrying the widest
536
+ spread in the table.
537
+
538
+ ![seed spread on the sealed test](benchmark/figures/13_final_test_seed_spread.png)
539
+
540
+ Every arm and every seed lands inside a 0.103-wide band on 55 documents, and for most arms the
541
+ distance between their own three seeds is larger than the distance to their neighbours. N=16,384
542
+ is the extreme: one seed at 0.5951 against two near 0.51.
543
+
544
+ ![per-label results for the published root](benchmark/figures/12_final_test_per_label.png)
545
+
546
+ Per label, F1 tracks support almost monotonically — `Loaded_Language` at 45 supporting documents
547
+ scores 0.918, and the six labels with 14 or fewer land between 0.446 and 0.481. Recall exceeds
548
+ precision on all fourteen labels: at the frozen thresholds, which run from 0.05 to 0.3, the head
549
+ over-predicts everywhere. A head trained for a deployment would be calibrated differently.
550
+
551
+ The v2 split is now retired from tuning and model selection under this protocol. v1 was not
552
+ opened, and its never-used status is not asserted.
553
+
554
+ Evidence: `REPORT.md` lines 1–89, `RESULTS.json` lines 1–742 (SHA-256
555
+ `0d2da5e2e8b250671bf3b19be0e7bce49f14fe492f9c71ce75717b0eee02dec7`), `PER_LABEL.csv`, and the
556
+ frozen preregistration (SHA-256 `85ec92a0be…`). All six are registered in this project's research
557
+ store, and the run is recorded there as `artifact_final_test_20260923_v1`.
558
+
559
  ## Two things we checked because they would have changed the reading
560
 
561
  ### Does the host matter?
 
753
 
754
  ## What this does not tell you
755
 
756
+ - **The one test evaluation is not a holdout.** The 2026-09-23 run above used the v2 re-split,
757
+ which this project had used before; its frozen preregistration records that prior exposure.
758
+ Every other number on this card is the opened 56-document calibration split, and there is no
759
+ `test.jsonl` in this repository.
760
  - **The evaluation is small.** 56 documents, 14 labels, and the rarest label appears in 6 of
761
  them, so one fourteenth of macro F1 rides on six samples. This is the most likely source of the
762
  0.005–0.039 seed SDs.
 
779
  | root | the 39,866-entry headless backbone |
780
  | [`root_manifest.json`](root_manifest.json) | its vocabulary rule, layer maps, hashes, why this grid point |
781
  | [`benchmark/full/`](benchmark/full/) | the 61-arm table, the grid, the same-host cuts |
782
+ | [`benchmark/figures/`](benchmark/figures/) | the seventeen figures above |
783
  | [`runtime/`](runtime/) | the two relaxed sources of the dedicated runtime, and its own README |
784
  | [`docker/`](docker/) | a container that runs the backbone with a head on it, CPU only |
785
  | [`docker-train/`](docker-train/) | train a classifier on your own data, convert, serve |
README_KO.md CHANGED
@@ -71,6 +71,10 @@ Qwen3.5-0.8B에서 증류한 **헤드 없는 4계층 텍스트 백본**이며,
71
  분리해 내지는 않았으므로, 그것은 짐작으로 남습니다. 절단과 깊이가 실제로 움직인 것은 파일과
72
  에너지이며, 그것은 위의 패널에 보입니다.
73
 
 
 
 
 
74
  ---
75
 
76
  ## 컨테이너로 실행하기
@@ -415,8 +419,118 @@ p50은 40x(5.7 ms에서 228.9 ms), 가중치 파일은 64x(22 MiB에서 1,435 Mi
415
  ![상위 arm들이 겹친다](benchmark/figures/02_top_arms_overlap.png)
416
  ![깊이](benchmark/figures/03_depth_quality_and_cost.png)
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  ---
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  ## 해석을 바꿀 수 있었기에 확인한 두 가지
421
 
422
  ### 호스트가 영향을 주는가?
@@ -609,8 +723,10 @@ A/B/A, 스레드 수 1/3/4, 라이브 서비스 경로도 모두 일치했습니
609
 
610
  ## 이것이 말해 주지 않는 것
611
 
612
- - **홀드아웃 평가가 없습니다.** 여기의 모든 수치는 열어 본 56개 문서 캘리브레이션 분할입니다.
613
- 저장소에`test.jsonl`없습니다.
 
 
614
  - **평가가 작습니다.** 문서 56개, 레이블 14개, 그리고 가장 드문 레이블은 그중 6개에 나타나므로,
615
  macro F1의 14분의 1이 표본 여섯 개에 걸려 있습니다. 이것이 0.005–0.039 시드 SD의 가장 유력한
616
  원인입니다.
@@ -632,7 +748,7 @@ A/B/A, 스레드 수 1/3/4, 라이브 서비스 경로도 모두 일치했습니
632
  | root | 39,866항목 헤드 없는 백본 |
633
  | [`root_manifest.json`](root_manifest.json) | 그 어휘 규칙, 계층 맵, 해시, 왜 이 그리드 포인트인지 |
634
  | [`benchmark/full/`](benchmark/full/) | 61개 arm 표, 그리드, 동일 호스트 절단 |
635
- | [`benchmark/figures/`](benchmark/figures/) | 위의 열 개 그림 |
636
  | [`runtime/`](runtime/) | 전용 런타임의 완화된 두 소스와 그 자신의 README |
637
  | [`docker/`](docker/) | 헤드를 얹은 백본을 돌리는 컨테이너, CPU 전용 |
638
  | [`docker-train/`](docker-train/) | 자기 데이터로 분류기를 학습시키고, 변환하고, 서빙 |
 
71
  분리해 내지는 않았으므로, 그것은 짐작으로 남습니다. 절단과 깊이가 실제로 움직인 것은 파일과
72
  에너지이며, 그것은 위의 패널에 보입니다.
73
 
74
+ 이 표의 모든 값은 열어 본 캘리브레이션 분할입니다. 고정 프로토콜 테스트 평가는 2026-09-23에 한 번,
75
+ 이 프로젝트가 전에 사용한 적 있는 재분할을 대상으로 수행했으며 — 한 번도 본 적 없는 홀드아웃이
76
+ 아닙니다 — 내려간 값까지 포함해 [**최종 테스트**](#최종-테스트)에 전부 보고했습니다.
77
+
78
  ---
79
 
80
  ## 컨테이너로 실행하기
 
419
  ![상위 arm들이 겹친다](benchmark/figures/02_top_arms_overlap.png)
420
  ![깊이](benchmark/figures/03_depth_quality_and_cost.png)
421
 
422
+ ### 전체 판이 어떻게 생겼는가
423
+
424
+ ![65개 arm의 품질 대 비용](benchmark/figures/14_field_quality_vs_cost.png)
425
+
426
+ 두 패널은 같은 65개 arm을 담고 있으며, 비용 축만 다릅니다. 점선은 파레토 경계 — 품질과 비용을
427
+ 동시에 이기는 다른 arm이 없는 arm들 — 입니다. 지연 기준으로는 일곱 개가, 에너지 기준으로는 여덟
428
+ 개가 경계를 이루고, 두 경계의 싼 쪽 절반은 모두 RoBERTa 계열이 차지합니다. 이 루트는 품질 축의
429
+ 위쪽에 있고, 그 자리에 있기 위해 `roberta-base`의 약 2.7배 지연을 지불합니다.
430
+
431
+ ![판에서 실제로 변하는 것](benchmark/figures/15_field_metric_spans.png)
432
+
433
+ 나란히 놓으면 이 폭 자체가 결과입니다. 판 전체에서 macro F1은 **1.2배** — 0.4756에서 0.5781까지 —
434
+ 움직이는 동안, 가중치 파일은 **64.4배**, GPU 에너지는 **42.9배**, 문서 지연은 **40.3배**
435
+ 움직입니다. 품질의 폭은 그 숫자가 보여 주는 것보다도 좁습니다. 그 아래 끝이 정확히 전부 양성으로
436
+ 답하는 예측기이므로, 판 전체가 "무엇이든 예"라고 답하는 모델 위 0.093의 유효 범위 안에 들어갑니다.
437
+ peak RSS는 거의 움직이지 않는데, 이는 가중치가 아니라 하네스가 그 값을 지배하기 때문입니다.
438
+
439
+ ![인코더 계열별 깊이 효과](benchmark/figures/16_field_depth_effect.png)
440
+
441
+ 모든 인코더 계열을 같은 프로토콜로 4, 6, 8 계층으로 잘랐고, 각 계열의 전층 체크포인트를 그 옆에
442
+ 그대로 두었습니다. **12개 계열 중 7개에서 가장 좋은 arm은 가장 깊은 것이 아닙니다.** `bert-base`,
443
+ `mbert`, `electra-base`는 4계층에서, `minilm-multilingual`은 6계층에서, `mpnet-base`와
444
+ `mdeberta-v3`는 8계층에서 정점을 찍습니다. 깊이가 값을 하는 다섯 계열 — roberta, xlmr, deberta-v3,
445
+ albert, modernbert — 은 그 대가를 지연과 파일 크기로 치릅니다. 세 개의 arm은 공유 프로토콜 아래에서
446
+ 아예 학습되지 않았고 정확히 바닥에 앉아 있습니다.
447
+
448
+ 이것은 이 과제를, 이 해상도에서 관찰한 결과입니다. 깊이가 중요하지 않다는 주장이 아니며, 이 루트가
449
+ 네 계층인 이유도 아닙니다. 그 이유는 3절에 있습니다.
450
+
451
+ ![65개 arm 전부를 한 축 위에](benchmark/figures/17_field_resolution_floor.png)
452
+
453
+ 마지막 그림이 이 순위에 대한 정직한 요약입니다. 판 전체의 3시드 SD 중앙값은 0.0116이고, 여섯 개의
454
+ arm이 최고점으로부터 그 한 개 SD 안에 들어 있습니다. 몇몇 SD 막대는 상위 열 개 arm 사이의 간격
455
+ 전체보다도 넓습니다. 이 순위는 측정했기 때문에 보고하는 것이지, 측정이 그것을 가려냈기 때문이
456
+ 아닙니다.
457
+
458
  ---
459
 
460
+ ---
461
+
462
+ ## 최종 테스트
463
+
464
+ 2026-09-23에 SemEval-2020 Task 11 **v2 테스트 재분할**을 대상으로 고정 프로토콜 평가를 한 번
465
+ 수행했습니다. 사전등록 문서는 테스트 바이트를 한 개도 읽기 전에 동결되었습니다.
466
+
467
+ **이것은 한 번도 본 적 없는 홀드아웃이 아닙니다.** 이 프로젝트는 v2 분할을 전에 사용한 적이 있고,
468
+ 동결된 문서는 이전 테스트 노출을 `true`로 기록하고 있습니다. 공식 리더보드 테스트가 아니며, 보안
469
+ 하네스 증명도 없고, 여기서 한국어 품질이나 일반적인 의도 분류 성능이 확립되지도 않습니다.
470
+ 사전등록 초안은 이 분할이 한 번도 열린 적 없다고 잘못 적었는데, 그 초안은 삭제하지 않고 수정본
471
+ 옆에 그대로 보관하고 있습니다.
472
+
473
+ 동결이 미리 고정한 것은 다음과 같습니다. 열한 개 arm의 명단, arm·시드·정밀도별 결정 임계값 — 각각
474
+ 그 arm의 기존 캘리브레이션 보고서에서 읽어 온 값이며 테스트에서 다시 계산하지 않았습니다 — 집계
475
+ 규칙, 지표 집합, 그리고 어떤 결과가 나오든 전부 공개한다는 규칙입니다. 보관된 페이로드는 추출
476
+ 전에 SHA-256 `ed224269dc…`와 일치해야 했고, 일치했습니다. 31개 작업 중 31개가 완료되었으며, 문서
477
+ 55개, 레이블 14개, 실패 0건, 학습 없음, 임계값 탐색 없음입니다. 이후 31개 실행의 지표 전부를 저장된
478
+ 확률로부터 별도의 표준 라이브러리 구현으로 다시 계산했고, 평가기와 일치했습니다.
479
+
480
+ ![캘리브레이션에서 봉인 테스트로](benchmark/figures/11_final_test_calibration_to_test.png)
481
+
482
+ | arm | 정밀도 | 캘리브레이션 | 테스트 | Δ |
483
+ |---|---|---:|---:|---:|
484
+ | **task-blind 4L N=32,768 — 이 루트의 분류기** | BF16 | 0.5719 ± 0.0092 | **0.5988 ± 0.0103** | **+0.0269** |
485
+ | task-blind 4L N=32,768 | Q8_0 | 0.5849 (시드 41) | 0.5862 (시드 41) | +0.0013 |
486
+ | task-blind 4L N=65,536 | BF16 | 0.5775 ± 0.0076 | 0.5832 ± 0.0047 | +0.0057 |
487
+ | task-blind 4L N=16,384 | BF16 | 0.5787 ± 0.0021 | 0.5379 ± 0.0496 | −0.0408 |
488
+ | task-blind 4L N=8,192 | BF16 | 0.5699 ± 0.0160 | 0.5555 ± 0.0070 | −0.0144 |
489
+ | 과거 4L v128k | BF16 | 0.5637 ± 0.0321 | 0.5747 ± 0.0183 | +0.0110 |
490
+ | 과거 8L v248k | BF16 | 0.5686 ± 0.0212 | 0.5718 ± 0.0213 | +0.0032 |
491
+ | 과거 4L v248k | BF16 | 0.5602 ± 0.0391 | 0.5707 ± 0.0081 | +0.0105 |
492
+ | 과거 24L v248k | BF16 | 0.5487 ± 0.0228 | 0.5612 ± 0.0102 | +0.0125 |
493
+ | roberta-base 12L | BF16 | 0.5524 ± 0.0143 | 0.5883 ± 0.0109 | +0.0359 |
494
+ | roberta-base 6L | BF16 | 0.5427 ± 0.0139 | 0.5839 ± 0.0152 | +0.0411 |
495
+ | 전부 양성 예측기 | — | 0.4756 | **0.4866** | — |
496
+
497
+ 양자화는 테스트에서 macro F1 0.0013을 치렀습니다. 4절이 캘리브레이션에서 본 "측정되지 않는 정도"와
498
+ 같은 값이며, 이번에는 임계값을 맞추지 않은 분할에서 본 것입니다.
499
+
500
+ **+0.0269은 이 모델에 관한 결과가 아닙니다.** 열한 개 arm 중 아홉 개가 올랐고, 열한 개의 평균
501
+ 이동은 +0.0085이며, 가장 많이 오른 것은 RoBERTa 레퍼런스 *둘*입니다. 이 테스트 분할이 캘리브레이션
502
+ 분할보다 높게 나오게 만드는 무언가는 판 전체에 적용되므로, 이 이동은 어느 arm의 성질이 아니라
503
+ 분할 쌍의 성질입니다.
504
+
505
+ 어느 arm도 다른 arm과 분리되지 않습니다. 이 루트의 평균 ± SD 구간은 `roberta-base`(0.5883 ±
506
+ 0.0109), `roberta-base-6l`(0.5839 ± 0.0152), 그리고 과거 4L arm 둘과 겹칩니다. 구간을 벗어나는
507
+ 경우에도 N=65,536과는 0.0006, N=16,384와는 0.0010 차이인데, 이는 그 값을 만들어 낸 설계의 산포보다
508
+ 한두 자릿수 작은 간격입니다.
509
+
510
+ 내려간 arm도 둘 있습니다. N=8,192는 0.0144, N=16,384는 0.0408 떨어졌고, 후자는 표에서 가장 넓은
511
+ 산포를 함께 지고 있습니다.
512
+
513
+ ![봉인 테스트의 시드 산포](benchmark/figures/13_final_test_seed_spread.png)
514
+
515
+ 모든 arm의 모든 시드가 문서 55개 위에서 폭 0.103의 띠 안에 들어옵니다. 그리고 대부분의 arm에서는
516
+ 자기 시드 세 개 사이의 거리가 이웃 arm까지의 거리보다 큽니다. N=16,384가 극단적인 사례로, 한 시드는
517
+ 0.5951인데 나머지 둘은 0.51 부근입니다.
518
+
519
+ ![게시된 루트의 레이블별 결과](benchmark/figures/12_final_test_per_label.png)
520
+
521
+ 레이블별로 보면 F1은 거의 단조롭게 support를 따라갑니다. 지지 문서가 45개인 `Loaded_Language`는
522
+ 0.918이고, 14개 이하인 여섯 레이블은 0.446에서 0.481 사이에 놓입니다. 그리고 열네 개 레이블 전부에서
523
+ recall이 precision을 넘습니다. 0.05에서 0.3에 걸친 동결 임계값 아래에서 이 헤드는 어디서나
524
+ 과다예측합니다. 실제 배포를 위해 학습하는 헤드라면 다르게 캘리브레이션하실 것입니다.
525
+
526
+ v2 분할은 이 프로토콜 아래에서 튜닝과 모델 선택 용도로 이제 은퇴했습니다. v1은 열지 않았으며,
527
+ v1이 한 번도 쓰이지 않았다고 주장하지는 않습니다.
528
+
529
+ 증거: `REPORT.md` 1–89행, `RESULTS.json` 1–742행(SHA-256
530
+ `0d2da5e2e8b250671bf3b19be0e7bce49f14fe492f9c71ce75717b0eee02dec7`), `PER_LABEL.csv`, 그리고
531
+ 동결된 사전등록 문서(SHA-256 `85ec92a0be…`). 여섯 건 모두 이 프로젝트의 리서치 스토어에 등록되어
532
+ 있고, 이 실행은 그곳에 `artifact_final_test_20260923_v1`로 기록되어 있습니다.
533
+
534
  ## 해석을 바꿀 수 있었기에 확인한 두 가지
535
 
536
  ### 호스트가 영향을 주는가?
 
723
 
724
  ## 이것이 말해 주지 않는 것
725
 
726
+ - **단 한 번의 테스트 평가는 홀드아웃 아닙니다.** 2026-09-23 실행은 v2 분할 사용했고,
727
+ 이 프로젝트그 분할을 전에 사용한 적있으며, 동결된 사전등록 문서가 그 이전 노출을 기록하고
728
+ 있습니다. 이 카드의 다른 모든 수치는 열어 본 56개 문서 캘리브레이션 분할이고, 이 저장소에는
729
+ `test.jsonl`이 없습니다.
730
  - **평가가 작습니다.** 문서 56개, 레이블 14개, 그리고 가장 드문 레이블은 그중 6개에 나타나므로,
731
  macro F1의 14분의 1이 표본 여섯 개에 걸려 있습니다. 이것이 0.005–0.039 시드 SD의 가장 유력한
732
  원인입니다.
 
748
  | root | 39,866항목 헤드 없는 백본 |
749
  | [`root_manifest.json`](root_manifest.json) | 그 어휘 규칙, 계층 맵, 해시, 왜 이 그리드 포인트인지 |
750
  | [`benchmark/full/`](benchmark/full/) | 61개 arm 표, 그리드, 동일 호스트 절단 |
751
+ | [`benchmark/figures/`](benchmark/figures/) | 위의 열일곱 개 그림 |
752
  | [`runtime/`](runtime/) | 전용 런타임의 완화된 두 소스와 그 자신의 README |
753
  | [`docker/`](docker/) | 헤드를 얹은 백본을 돌리는 컨테이너, CPU 전용 |
754
  | [`docker-train/`](docker-train/) | 자기 데이터로 분류기를 학습시키고, 변환하고, 서빙 |
SHA256SUMS CHANGED
@@ -1,6 +1,6 @@
1
  77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 LICENSE
2
- b901b5be1af24ece943d174566a9c22e55019eef403081f9fc21672100456f50 README.md
3
- 9d99d92cf91bcffa39d794ee8f15b7517695f8f2f67311ae081010e37c153dee README_KO.md
4
  bf4073de38c3905effd2dcce6f38894f325c8a0a79bedfb2779f0545c86b9c9f benchmark/BENCHMARK_CARD.md
5
  e01bd7ac59a3212e44b77e5fcfbc7c41ec91d74c998306d2438209d6ef1a3aa7 benchmark/arm_summary.csv
6
  2c544c3907c4f592d724f658850b8c6b9634845cb5b50a66ebd2ea6c5d48ce0b benchmark/compression_ladder.csv
@@ -26,6 +26,13 @@ c9c09f7f91a79d992434262ba50e0f2cb92d34058da6be7e3468a3eef60fe131 benchmark/figu
26
  9519e737b99e44271434e9ea45c5fac583dc0ef16ecf3bc3deb3f0c71a39618e benchmark/figures/08_long_context_window.png
27
  c28ed833aef0025646d3d6c9163fbafeec84712e6941fb1c4590b9a47d55c84b benchmark/figures/09_tokenizer_cost.png
28
  56a7353a71dd727077a420fd39a97b21742b8af87fabcbd4d4cdbfe09f5a0b40 benchmark/figures/10_root_on_every_metric.png
 
 
 
 
 
 
 
29
  7c238ed969ed64ac7c63084c663ecc531b618622199a77b09d28f461db11a657 benchmark/figures/README.md
30
  afd203646513c49140a25ef2a562a064286a4fa5efb495e6a73f83f0106e7933 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.csv
31
  22f531ea586f9c79ac8f114345cec56e74ea5ed97aa3726f7310018666ae6312 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.pdf
@@ -39,6 +46,7 @@ aac7d3400cf8dcb3ac8b6fc317c27ded5343231c93476997011a3bb56b5e4d6c benchmark/figu
39
  f3e79965f14ec0fd20d11c361d25c3a3abc4c9a20b5dbce9fac0660bb12789f0 benchmark/figures/cross_task/README.md
40
  c3312e98b8abbbdc488e5dfde77c4110732e8d08abe9ed931bd985ad799ecad8 benchmark/figures/fig10.py
41
  3f6b1b82af1619afab8c753cd836040a7353a3226646b8b99fb641617e4ecec7 benchmark/figures/make_more_figures.py
 
42
  078b4f34897e0a1b9bcff66ce21ba0feb28d72c86eaa8cef608f110c48f52f2e benchmark/figures/v128k_compression.csv
43
  014da12a435e3404b31470e2642d180dc111eb608fb8513fd993097019698b7a benchmark/figures/v128k_quality_efficiency.csv
44
  644e5b7469c0b4e0b63d5ecaa1c0f2fc8e07af8bb5d9e4d3ae5a88166de374f3 benchmark/figures/v128k_quality_storage.png
 
1
  77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 LICENSE
2
+ 95f3404c3d41a691e4de347b7cf5b510769a23763808dc9a9a713b72340e765c README.md
3
+ d3aa51ff96c38b1bfb6fa2391591f7a4cf95357418c43a95f88ef9545d372608 README_KO.md
4
  bf4073de38c3905effd2dcce6f38894f325c8a0a79bedfb2779f0545c86b9c9f benchmark/BENCHMARK_CARD.md
5
  e01bd7ac59a3212e44b77e5fcfbc7c41ec91d74c998306d2438209d6ef1a3aa7 benchmark/arm_summary.csv
6
  2c544c3907c4f592d724f658850b8c6b9634845cb5b50a66ebd2ea6c5d48ce0b benchmark/compression_ladder.csv
 
26
  9519e737b99e44271434e9ea45c5fac583dc0ef16ecf3bc3deb3f0c71a39618e benchmark/figures/08_long_context_window.png
27
  c28ed833aef0025646d3d6c9163fbafeec84712e6941fb1c4590b9a47d55c84b benchmark/figures/09_tokenizer_cost.png
28
  56a7353a71dd727077a420fd39a97b21742b8af87fabcbd4d4cdbfe09f5a0b40 benchmark/figures/10_root_on_every_metric.png
29
+ 71aa42d84f71dfc975a6906fc84e6f39ed934bf3ed3038aadbc1ad924ca2bf21 benchmark/figures/11_final_test_calibration_to_test.png
30
+ cfb3071db4077bed7aee06c141f5f12873779a732516242580c889fb223c14fa benchmark/figures/12_final_test_per_label.png
31
+ 4a9704a2f64b58f43b7feab7b845c0fd860f45fc3a2a2ea6ae996473c9eafff4 benchmark/figures/13_final_test_seed_spread.png
32
+ 5d2963b051a74a71c63ed0d226352b207ca501d3a356d99c32e832217fc51f05 benchmark/figures/14_field_quality_vs_cost.png
33
+ 4639fc800d53bec745bc9f1d5df37a6177f06116689798738c3677b2f05d303a benchmark/figures/15_field_metric_spans.png
34
+ 5da006b4d603cc858c480a3cff94d9584327170933268fbac8ce8f131db87fcf benchmark/figures/16_field_depth_effect.png
35
+ dc621e266a694496ba30d9d5c03e89e4d6c65616f5c25df0216d012102cd3f53 benchmark/figures/17_field_resolution_floor.png
36
  7c238ed969ed64ac7c63084c663ecc531b618622199a77b09d28f461db11a657 benchmark/figures/README.md
37
  afd203646513c49140a25ef2a562a064286a4fa5efb495e6a73f83f0106e7933 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.csv
38
  22f531ea586f9c79ac8f114345cec56e74ea5ed97aa3726f7310018666ae6312 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.pdf
 
46
  f3e79965f14ec0fd20d11c361d25c3a3abc4c9a20b5dbce9fac0660bb12789f0 benchmark/figures/cross_task/README.md
47
  c3312e98b8abbbdc488e5dfde77c4110732e8d08abe9ed931bd985ad799ecad8 benchmark/figures/fig10.py
48
  3f6b1b82af1619afab8c753cd836040a7353a3226646b8b99fb641617e4ecec7 benchmark/figures/make_more_figures.py
49
+ 5f6b1979dcb7ff4ee897659b2f902dd3cc36339987f42f681bff7be5ae06d304 benchmark/figures/make_readout_figures.py
50
  078b4f34897e0a1b9bcff66ce21ba0feb28d72c86eaa8cef608f110c48f52f2e benchmark/figures/v128k_compression.csv
51
  014da12a435e3404b31470e2642d180dc111eb608fb8513fd993097019698b7a benchmark/figures/v128k_quality_efficiency.csv
52
  644e5b7469c0b4e0b63d5ecaa1c0f2fc8e07af8bb5d9e4d3ae5a88166de374f3 benchmark/figures/v128k_quality_storage.png
benchmark/figures/11_final_test_calibration_to_test.png ADDED

Git LFS Details

  • SHA256: 71aa42d84f71dfc975a6906fc84e6f39ed934bf3ed3038aadbc1ad924ca2bf21
  • Pointer size: 131 Bytes
  • Size of remote file: 199 kB
benchmark/figures/12_final_test_per_label.png ADDED

Git LFS Details

  • SHA256: cfb3071db4077bed7aee06c141f5f12873779a732516242580c889fb223c14fa
  • Pointer size: 131 Bytes
  • Size of remote file: 190 kB
benchmark/figures/13_final_test_seed_spread.png ADDED

Git LFS Details

  • SHA256: 4a9704a2f64b58f43b7feab7b845c0fd860f45fc3a2a2ea6ae996473c9eafff4
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
benchmark/figures/14_field_quality_vs_cost.png ADDED

Git LFS Details

  • SHA256: 5d2963b051a74a71c63ed0d226352b207ca501d3a356d99c32e832217fc51f05
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
benchmark/figures/15_field_metric_spans.png ADDED

Git LFS Details

  • SHA256: 4639fc800d53bec745bc9f1d5df37a6177f06116689798738c3677b2f05d303a
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
benchmark/figures/16_field_depth_effect.png ADDED

Git LFS Details

  • SHA256: 5da006b4d603cc858c480a3cff94d9584327170933268fbac8ce8f131db87fcf
  • Pointer size: 131 Bytes
  • Size of remote file: 206 kB
benchmark/figures/17_field_resolution_floor.png ADDED

Git LFS Details

  • SHA256: dc621e266a694496ba30d9d5c03e89e4d6c65616f5c25df0216d012102cd3f53
  • Pointer size: 131 Bytes
  • Size of remote file: 314 kB
benchmark/figures/make_readout_figures.py ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Figures for the model card: the sealed final test, and the 65-arm field.
2
+
3
+ Run from benchmark/figures/ . Figures 14-17 need only benchmark/full/*.csv, which ships here.
4
+
5
+ Sources, all read-only:
6
+ final test agents/sessions/2026-09-23/S-20260923-final-test-preflight-v1/{RESULTS,PER_LABEL}.csv
7
+ ... BASELINE.json
8
+ 65-arm field benchmark/full/61_arm_bf16.csv + benchmark/full/taskblind_grid.csv
9
+
10
+ Writes PNGs next to this file.
11
+ """
12
+ import csv
13
+ import json
14
+ import os
15
+ import re
16
+ import collections
17
+ import math
18
+ import statistics as st
19
+
20
+ import matplotlib
21
+ matplotlib.use("Agg")
22
+ import matplotlib.pyplot as plt
23
+ from matplotlib.lines import Line2D
24
+ from matplotlib.patches import Patch
25
+ from matplotlib.colors import LinearSegmentedColormap, Normalize
26
+ from matplotlib.cm import ScalarMappable
27
+
28
+ # Paths are resolved relative to this file's repository, or overridden by env vars.
29
+ HERE = os.path.dirname(os.path.abspath(__file__))
30
+ REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
31
+ # Final-test inputs are not redistributed in this repository; point SESS at a local copy of
32
+ # agents/sessions/2026-09-23/S-20260923-final-test-preflight-v1/ to rebuild figures 11-13.
33
+ SESS = os.environ.get("FINAL_TEST_DIR", os.path.join(REPO, "final_test")) + os.sep
34
+ BENCH = os.path.join(REPO, "benchmark", "full") + os.sep
35
+ OUT = os.path.join(HERE, "")
36
+
37
+ FLOOR_TEST = json.load(open(SESS + "BASELINE.json"))["always_positive"]["macro_f1"]
38
+ FLOOR_CAL = 0.475627 # all-positive predictor on the opened calibration split
39
+
40
+ INK, MUTED, FAINT = "#1a1a1a", "#6a6a72", "#9a9aa2"
41
+ GRID = "#e6e6ea"
42
+ # Series hues, checked pairwise in OKLab and under deuter/prot/trit simulation:
43
+ # minimum normal-vision dE 18.3, minimum CVD dE 8.6, all >= 30 against white.
44
+ ROOTC, GRIDC, QWENC, ENCC = "#1a5496", "#0d7a52", "#b05512", "#9a9aa2"
45
+ # Status, never a series colour: always carries a dash pattern, an arrow direction,
46
+ # a marker shape or a text label as well.
47
+ DOWN = "#c0392b"
48
+ ACC = QWENC
49
+
50
+
51
+ def style(ax, xgrid=True, ygrid=False):
52
+ ax.grid(axis="both" if (xgrid and ygrid) else ("x" if xgrid else "y"),
53
+ color=GRID, lw=.8)
54
+ ax.set_axisbelow(True)
55
+ for s in ("top", "right"):
56
+ ax.spines[s].set_visible(False)
57
+ for s in ("bottom", "left"):
58
+ ax.spines[s].set_color("#c9c9cf")
59
+ ax.tick_params(length=0, colors="#4a4a52")
60
+
61
+
62
+ def footer(fig, text):
63
+ fig.text(.006, .012, text, fontsize=8.6, color=MUTED, ha="left")
64
+
65
+
66
+ # ---------------------------------------------------------------- final test
67
+ FT_NAMES = {
68
+ "qwen35-taskblind-base4l-N32768-commonhead": "task-blind 4L N=32,768",
69
+ "qwen35-taskblind-base4l-N65536-commonhead": "task-blind 4L N=65,536",
70
+ "qwen35-taskblind-base4l-N16384-commonhead": "task-blind 4L N=16,384",
71
+ "qwen35-taskblind-base4l-N8192-commonhead": "task-blind 4L N=8,192",
72
+ "qwen35-taskfree-base24l-v248k-commonhead": "historical 24L v248k",
73
+ "qwen35-taskfree-base8l-v248k-commonhead": "historical 8L v248k",
74
+ "qwen35-taskfree-base4l-v248k-commonhead": "historical 4L v248k",
75
+ "qwen35-taskfree-base4l-v128k-commonhead": "historical 4L v128k",
76
+ "roberta-base-commonhead": "RoBERTa-base 12L",
77
+ "roberta-base-6l-commonhead": "RoBERTa-base 6L",
78
+ }
79
+ FT_FAM = {"published": (ROOTC, "published root (N=32,768)"),
80
+ "grid": (GRIDC, "other task-blind grid points"),
81
+ "historical": (ENCC, "historical qwen arms"),
82
+ "roberta": (QWENC, "RoBERTa references")}
83
+
84
+
85
+ def ft_family(arm):
86
+ if arm == "qwen35-taskblind-base4l-N32768-commonhead":
87
+ return "published"
88
+ if arm.startswith("qwen35-taskblind"):
89
+ return "grid"
90
+ if arm.startswith("roberta"):
91
+ return "roberta"
92
+ return "historical"
93
+
94
+
95
+ def ft_arms():
96
+ rows = list(csv.DictReader(open(SESS + "RESULTS.csv")))
97
+ g = collections.defaultdict(list)
98
+ for r in rows:
99
+ g[(r["arm_id"], r["precision"])].append(r)
100
+ arms = []
101
+ for (a, p), rs in g.items():
102
+ cal = [float(x["calibration_macro_f1"]) for x in rs]
103
+ tst = [float(x["test_macro_f1"]) for x in rs]
104
+ arms.append({"label": FT_NAMES[a] + (" Q8_0" if p != "BF16" else ""),
105
+ "fam": ft_family(a), "n": len(rs),
106
+ "cal": st.mean(cal), "test": st.mean(tst),
107
+ "seeds": sorted(tst),
108
+ "sd": st.stdev(tst) if len(tst) > 1 else 0.0})
109
+ arms.sort(key=lambda d: d["test"])
110
+ return arms
111
+
112
+
113
+ def fig_calibration_to_test():
114
+ arms = ft_arms()
115
+ fig, ax = plt.subplots(figsize=(10.6, 6.4))
116
+ fig.patch.set_facecolor("white")
117
+ XMIN, XMAX, XVAL = .468, .646, .624
118
+ for i, d in enumerate(arms):
119
+ c = FT_FAM[d["fam"]][0]
120
+ up = d["test"] >= d["cal"]
121
+ if d["sd"]:
122
+ ax.plot([d["test"] - d["sd"], d["test"] + d["sd"]], [i, i],
123
+ color=c, lw=7, alpha=.20, solid_capstyle="butt", zorder=2)
124
+ ax.annotate("", xy=(d["test"], i), xytext=(d["cal"], i),
125
+ arrowprops=dict(arrowstyle="-|>", lw=2.0,
126
+ color=c if up else DOWN, shrinkA=0, shrinkB=0, alpha=.9))
127
+ ax.plot([d["cal"]], [i], "o", ms=7, mfc="white", mec=c, mew=2.0, zorder=3)
128
+ ax.plot([d["test"]], [i], "o", ms=9.5, color=c, zorder=4,
129
+ mec="white", mew=1.6)
130
+ bold = "bold" if d["fam"] == "published" else "normal"
131
+ ax.text(XVAL, i, f"{d['test']:.4f}", va="center", ha="right",
132
+ fontsize=9.5, color=INK, fontweight=bold)
133
+ dl = d["test"] - d["cal"]
134
+ ax.text(XMAX - .002, i, f"{dl:+.4f}", va="center", ha="right", fontsize=9,
135
+ color=DOWN if dl < 0 else "#4a7a4a", fontweight=bold)
136
+ ax.axvline(FLOOR_TEST, color=DOWN, ls=(0, (5, 3)), lw=1.6, zorder=1)
137
+ ax.text(FLOOR_TEST + .003, -.62, f"all-positive floor {FLOOR_TEST:.4f}",
138
+ va="bottom", ha="left", fontsize=8.8, color=DOWN)
139
+ ax.text(XVAL, len(arms) - .34, "test", ha="right", va="bottom", fontsize=8.6, color=FAINT)
140
+ ax.text(XMAX - .002, len(arms) - .34, "Δ", ha="right", va="bottom",
141
+ fontsize=8.6, color=FAINT)
142
+ ax.set_yticks(range(len(arms)))
143
+ ax.set_yticklabels([d["label"] for d in arms], fontsize=9.5)
144
+ for t, d in zip(ax.get_yticklabels(), arms):
145
+ if d["fam"] == "published":
146
+ t.set_fontweight("bold")
147
+ ax.set_xlabel("macro F1 (14-label fixed denominator)", fontsize=10)
148
+ ax.set_xlim(XMIN, XMAX)
149
+ ax.set_ylim(-.95, len(arms) - .15)
150
+ ax.set_xticks([.48, .50, .52, .54, .56, .58, .60])
151
+ style(ax, xgrid=True)
152
+ ax.spines["left"].set_visible(False)
153
+ handles = [Line2D([], [], marker="o", ls="", color=c, ms=8, label=l)
154
+ for c, l in FT_FAM.values()]
155
+ handles.append(Line2D([], [], color=DOWN, lw=2.2, label="moved down on test"))
156
+ ax.legend(handles=handles, fontsize=8.6, frameon=False, ncol=1,
157
+ loc="center left", bbox_to_anchor=(.005, .30), labelspacing=.55)
158
+ ax.set_title("Calibration → sealed test\n"
159
+ "hollow = calibration mean, filled = test mean, band = sample SD over 3 seeds",
160
+ fontsize=11.5, loc="left", color=INK, pad=12)
161
+ footer(fig, "Nine of eleven arms moved up and the two RoBERTa references moved up the most, "
162
+ "so the direction belongs to the split pair, not to any model.")
163
+ fig.tight_layout(rect=(0, .035, 1, 1))
164
+ fig.savefig(OUT + "11_final_test_calibration_to_test.png", dpi=170, facecolor="white")
165
+ plt.close(fig)
166
+
167
+
168
+ def fig_per_label():
169
+ pl = [r for r in csv.DictReader(open(SESS + "PER_LABEL.csv"))
170
+ if r["job_id"].startswith("qwen35-taskblind-base4l-N32768-commonhead")
171
+ and r["job_id"].endswith("BF16")]
172
+ by = collections.defaultdict(list)
173
+ for r in pl:
174
+ by[r["label"]].append(r)
175
+ num = lambda v: float(v) if v not in ("", "null", None) else None
176
+ pts = []
177
+ for lab, rs in by.items():
178
+ pts.append({"lab": lab.replace("_", " ").replace(",", " / "),
179
+ "sup": int(rs[0]["support"]),
180
+ "f1": st.mean([num(r["f1"]) for r in rs]),
181
+ "p": st.mean([num(r["precision"]) for r in rs]),
182
+ "r": st.mean([num(r["recall"]) for r in rs])})
183
+ pts.sort(key=lambda d: d["f1"])
184
+ ramp = LinearSegmentedColormap.from_list("sup", ["#cfe0f0", "#14406f"])
185
+ norm = Normalize(vmin=min(p["sup"] for p in pts), vmax=max(p["sup"] for p in pts))
186
+ fig, ax = plt.subplots(figsize=(10.2, 6.2))
187
+ fig.patch.set_facecolor("white")
188
+ ax.barh(range(len(pts)), [p["f1"] for p in pts], height=.64,
189
+ color=[ramp(norm(p["sup"])) for p in pts], zorder=3)
190
+ for i, p in enumerate(pts):
191
+ ax.text(p["f1"] - .012, i, f"{p['f1']:.3f}", va="center", ha="right",
192
+ fontsize=8.8, color="white", fontweight="bold", zorder=4)
193
+ ax.text(p["f1"] + .014, i, f"n={p['sup']} P {p['p']:.2f} / R {p['r']:.2f}",
194
+ va="center", ha="left", fontsize=8.4, color="#5a5a62", zorder=4)
195
+ macro = st.mean([p["f1"] for p in pts])
196
+ ax.axvline(macro, color="#14406f", ls=(0, (5, 3)), lw=1.5, zorder=2)
197
+ ax.text(macro + .006, len(pts) - .42, f"macro F1 {macro:.4f}",
198
+ fontsize=9, color="#14406f", ha="left", va="bottom")
199
+ ax.set_yticks(range(len(pts)))
200
+ ax.set_yticklabels([p["lab"] for p in pts], fontsize=9)
201
+ ax.set_xlabel("per-label F1 (3-seed mean)", fontsize=10)
202
+ ax.set_xlim(0, 1.16)
203
+ ax.set_ylim(-.7, len(pts) - .15)
204
+ ax.set_xticks([0, .2, .4, .6, .8, 1.0])
205
+ style(ax, xgrid=True)
206
+ ax.spines["left"].set_visible(False)
207
+ ax.set_title("Published root on the sealed test, per label\n"
208
+ "bar shade = support in the 55 test documents",
209
+ fontsize=11.5, loc="left", color=INK, pad=12)
210
+ cb = fig.colorbar(ScalarMappable(norm=norm, cmap=ramp), ax=ax,
211
+ orientation="horizontal", fraction=.030, pad=.12, aspect=44)
212
+ cb.set_label("label support (documents)", fontsize=8.6, color=MUTED)
213
+ cb.ax.tick_params(labelsize=8, length=0, colors=MUTED)
214
+ cb.outline.set_visible(False)
215
+ footer(fig, "Recall exceeds precision on all fourteen labels: at these thresholds the head "
216
+ "over-predicts. F1 tracks support almost monotonically.")
217
+ fig.tight_layout(rect=(0, .035, 1, 1))
218
+ fig.savefig(OUT + "12_final_test_per_label.png", dpi=170, facecolor="white")
219
+ plt.close(fig)
220
+
221
+
222
+ def fig_seed_spread():
223
+ arms = [a for a in ft_arms() if a["n"] == 3]
224
+ arms.sort(key=lambda d: d["test"])
225
+ fig, ax = plt.subplots(figsize=(10.6, 5.8))
226
+ fig.patch.set_facecolor("white")
227
+ for i, d in enumerate(arms):
228
+ c = FT_FAM[d["fam"]][0]
229
+ ax.plot([min(d["seeds"]), max(d["seeds"])], [i, i], color=c, lw=2, alpha=.35,
230
+ solid_capstyle="round", zorder=2)
231
+ for s in d["seeds"]:
232
+ ax.plot([s], [i], "o", ms=7, color=c, alpha=.85, zorder=3, mec="white", mew=1.2)
233
+ ax.plot([d["test"]], [i], "|", ms=17, color=INK, mew=2.0, zorder=4)
234
+ ax.text(.638, i, f"spread {max(d['seeds']) - min(d['seeds']):.4f}",
235
+ va="center", ha="right", fontsize=8.8,
236
+ color=DOWN if max(d["seeds"]) - min(d["seeds"]) > .05 else MUTED)
237
+ ax.set_yticks(range(len(arms)))
238
+ ax.set_yticklabels([d["label"] for d in arms], fontsize=9.5)
239
+ ax.set_xlabel("test macro F1, one dot per seed (41 / 42 / 43); tick = mean", fontsize=10)
240
+ ax.set_xlim(.478, .642)
241
+ ax.set_ylim(-.7, len(arms) - .3)
242
+ style(ax, xgrid=True)
243
+ ax.spines["left"].set_visible(False)
244
+ ax.set_title("Seed spread swamps the gaps between arms\n"
245
+ "every arm and seed lands inside a 0.103-wide band on 55 documents",
246
+ fontsize=11.5, loc="left", color=INK, pad=12)
247
+ footer(fig, "N=16,384 is the extreme case: one seed at 0.5951 and two near 0.51. "
248
+ "Three seeds give a sample SD, not a confidence interval.")
249
+ fig.tight_layout(rect=(0, .038, 1, 1))
250
+ fig.savefig(OUT + "13_final_test_seed_spread.png", dpi=170, facecolor="white")
251
+ plt.close(fig)
252
+
253
+
254
+ # ------------------------------------------------------------- the 65 arms
255
+ BASE_LAYERS = {"bert-base": 12, "roberta-base": 12, "xlmr-base": 12, "deberta-v3-base": 12,
256
+ "mdeberta-v3": 12, "albert-base-v2": 12, "electra-base": 12, "mpnet-base": 12,
257
+ "modernbert-base": 22, "distilbert-base": 6, "mbert": 12,
258
+ "minilm-multilingual": 12}
259
+
260
+
261
+ def encoder_family(arm_id):
262
+ s = arm_id.replace("-commonhead", "")
263
+ m = re.match(r"^(.*?)-(\d+)l$", s)
264
+ if m and m.group(1) in BASE_LAYERS:
265
+ return m.group(1), int(m.group(2))
266
+ if s in BASE_LAYERS:
267
+ return s, BASE_LAYERS[s]
268
+ return None, None
269
+
270
+
271
+ def field():
272
+ """61 measured arms plus the 4 task-blind grid points = 65."""
273
+ out = []
274
+ for r in csv.DictReader(open(BENCH + "61_arm_bf16.csv")):
275
+ fam, _ = encoder_family(r["arm_id"])
276
+ out.append({"id": r["arm_id"], "q": float(r["macro_f1_mean"]),
277
+ "sd": float(r["macro_f1_sd"]), "p50": float(r["p50_ms"]),
278
+ "mib": float(r["weight_file_mib"]), "j": float(r["gpu_j_per_doc"]),
279
+ "rss": float(r["peak_rss_mib"]),
280
+ "floor": r["degenerate_at_floor"] == "True",
281
+ "grp": "encoder" if fam else "qwen"})
282
+ for r in csv.DictReader(open(BENCH + "taskblind_grid.csv")):
283
+ n = int(r["grid_point_N"])
284
+ out.append({"id": f"qwen35-taskblind-base4l-N{n}", "q": float(r["macro_f1_mean"]),
285
+ "sd": float(r["macro_f1_sd"]), "p50": float(r["bf16_p50_ms"]),
286
+ "mib": float(r["root_mib"]), "j": float(r["bf16_gpu_j_per_doc"]),
287
+ "rss": float("nan"), "floor": False,
288
+ "grp": "root" if n == 32768 else "grid"})
289
+ return out
290
+
291
+
292
+ GRP = {"root": (ROOTC, "this root (N=32,768)"),
293
+ "grid": (GRIDC, "task-blind grid"),
294
+ "qwen": (QWENC, "other Qwen (3.5 and 2.5)"),
295
+ "encoder": (ENCC, "encoder families (10)")}
296
+
297
+
298
+ def pareto(rows, key):
299
+ keep = []
300
+ for r in rows:
301
+ if not any(o["q"] >= r["q"] and o[key] <= r[key] and
302
+ (o["q"] > r["q"] or o[key] < r[key]) for o in rows):
303
+ keep.append(r)
304
+ return sorted(keep, key=lambda r: r[key])
305
+
306
+
307
+ def fig_quality_vs_cost():
308
+ F = field()
309
+ fig, axes = plt.subplots(1, 2, figsize=(14.6, 6.2), sharey=True)
310
+ fig.patch.set_facecolor("white")
311
+ for ax, key, xlab, unit in (
312
+ (axes[0], "p50", "document p50 latency (ms, log scale)", "ms"),
313
+ (axes[1], "j", "GPU energy per document (J, log scale)", "J")):
314
+ front = pareto(F, key)
315
+ ax.plot([r[key] for r in front], [r["q"] for r in front],
316
+ color=INK, lw=1.3, alpha=.45, zorder=2, ls=(0, (4, 3)))
317
+ for grp in ("encoder", "qwen", "grid", "root"):
318
+ pts = [r for r in F if r["grp"] == grp]
319
+ if not pts:
320
+ continue
321
+ big = grp == "root"
322
+ ax.scatter([r[key] for r in pts], [r["q"] for r in pts],
323
+ s=150 if big else 46, color=GRP[grp][0], zorder=5 if big else 3,
324
+ edgecolor="white", linewidth=1.6 if big else .9,
325
+ marker="D" if big else "o", alpha=1 if big else .92)
326
+ ax.axhline(FLOOR_CAL, color=DOWN, ls=(0, (5, 3)), lw=1.4, zorder=1)
327
+ ax.set_xscale("log")
328
+ ax.set_xlabel(xlab, fontsize=10)
329
+ style(ax, xgrid=True, ygrid=True)
330
+ labelled = []
331
+ for j, r in enumerate(front):
332
+ if r["grp"] in ("root", "grid"):
333
+ continue
334
+ nm = r["id"].replace("-commonhead", "").replace("qwen35-", "")
335
+ # skip a label that would land on top of one already placed
336
+ if any(abs(math.log10(r[key]) - math.log10(o)) < .17 and abs(r["q"] - q) < .012
337
+ for o, q in labelled):
338
+ continue
339
+ labelled.append((r[key], r["q"]))
340
+ ax.annotate(nm, (r[key], r["q"]), textcoords="offset points",
341
+ xytext=(9, -13 if j % 2 == 0 else 9), fontsize=7.6, color=MUTED)
342
+ rt = [r for r in F if r["grp"] == "root"][0]
343
+ ax.annotate("this root", (rt[key], rt["q"]), textcoords="offset points",
344
+ xytext=(13, 9), fontsize=8.8, color=ROOTC, fontweight="bold")
345
+ axes[0].set_ylabel("macro F1 (3-seed mean)", fontsize=10)
346
+ axes[0].set_ylim(.466, .582)
347
+ axes[0].text(6.1, FLOOR_CAL + .0018, f"all-positive floor {FLOOR_CAL:.4f}",
348
+ fontsize=8.5, color=DOWN, va="bottom")
349
+ handles = [Line2D([], [], marker="D" if k == "root" else "o", ls="", color=c,
350
+ ms=9 if k == "root" else 7, label=l) for k, (c, l) in GRP.items()]
351
+ handles.append(Line2D([], [], color=INK, lw=1.3, ls=(0, (4, 3)), alpha=.45,
352
+ label="Pareto frontier"))
353
+ handles.append(Line2D([], [], color=DOWN, lw=1.4, ls=(0, (5, 3)),
354
+ label="all-positive floor"))
355
+ fig.legend(handles=handles, fontsize=8.8, frameon=False, ncol=6,
356
+ loc="upper left", bbox_to_anchor=(.006, .945), columnspacing=1.5)
357
+ fig.suptitle("65 arms, one frozen protocol: quality barely moves, cost moves by orders of magnitude",
358
+ fontsize=12.5, x=.006, ha="left", y=.975, color=INK)
359
+ footer(fig, "Dashed line is the Pareto frontier. The RoBERTa family owns the cheap half of it; "
360
+ "the Qwen3.5 arms own the top. Every arm lives within 0.093 of a trivial all-positive predictor.")
361
+ fig.tight_layout(rect=(0, .038, 1, .895))
362
+ fig.savefig(OUT + "14_field_quality_vs_cost.png", dpi=170, facecolor="white")
363
+ plt.close(fig)
364
+
365
+
366
+ def fig_metric_spans():
367
+ F = field()
368
+ specs = [("macro F1", [r["q"] for r in F], "", 4),
369
+ ("peak RSS", [r["rss"] for r in F if r["rss"] == r["rss"]], "MiB", 0),
370
+ ("document p50", [r["p50"] for r in F], "ms", 0),
371
+ ("GPU energy / doc", [r["j"] for r in F], "J", 2),
372
+ ("weight file", [r["mib"] for r in F], "MiB", 0)]
373
+ rows = []
374
+ for name, vals, unit, dec in specs:
375
+ lo, hi = min(vals), max(vals)
376
+ rows.append((name, lo, hi, hi / lo if lo > 0 else float("inf"), unit, dec))
377
+ rows.sort(key=lambda t: t[3])
378
+ fig, ax = plt.subplots(figsize=(11.0, 4.4))
379
+ fig.patch.set_facecolor("white")
380
+ for i, (name, lo, hi, ratio, unit, dec) in enumerate(rows):
381
+ c = ROOTC if ratio > 5 else QWENC
382
+ ax.plot([1, ratio], [i, i], color=c, lw=9, solid_capstyle="round",
383
+ alpha=.85, zorder=3)
384
+ ax.text(ratio * 1.09, i, f"{ratio:.1f}×", va="center", ha="left",
385
+ fontsize=11, color=c, fontweight="bold")
386
+ ax.text(ratio * 1.09, i - .30, f"{lo:,.{dec}f} → {hi:,.{dec}f} {unit}".strip(),
387
+ va="center", ha="left", fontsize=8.3, color=MUTED)
388
+ ax.set_yticks(range(len(rows)))
389
+ ax.set_yticklabels([r[0] for r in rows], fontsize=10)
390
+ ax.set_xscale("log")
391
+ ax.set_xlim(.92, 260)
392
+ ax.set_ylim(-.7, len(rows) - .3)
393
+ ax.set_xlabel("ratio of the largest arm to the smallest, across all 65 arms (log scale)",
394
+ fontsize=10)
395
+ style(ax, xgrid=True)
396
+ ax.spines["left"].set_visible(False)
397
+ ax.set_title("What actually varies across the field\n"
398
+ "the answers are nearly identical; what they cost to produce is not",
399
+ fontsize=11.5, loc="left", color=INK, pad=12)
400
+ footer(fig, f"The best arm beats a trivial all-positive predictor ({FLOOR_CAL:.4f}) by 0.0930, "
401
+ "and the worst three tie it exactly. Peak RSS is dominated by the harness, not the model.")
402
+ fig.tight_layout(rect=(0, .05, 1, 1))
403
+ fig.savefig(OUT + "15_field_metric_spans.png", dpi=170, facecolor="white")
404
+ plt.close(fig)
405
+
406
+
407
+ def fig_depth_effect():
408
+ rows = list(csv.DictReader(open(BENCH + "61_arm_bf16.csv")))
409
+ fams = collections.defaultdict(list)
410
+ for r in rows:
411
+ fam, L = encoder_family(r["arm_id"])
412
+ if fam:
413
+ fams[fam].append((L, float(r["macro_f1_mean"]),
414
+ r["degenerate_at_floor"] == "True"))
415
+ order = sorted(fams, key=lambda k: -max(q for _, q, _ in fams[k]))
416
+ fig, axes = plt.subplots(3, 4, figsize=(14.2, 7.4), sharex=True, sharey=True)
417
+ fig.patch.set_facecolor("white")
418
+ shallow_wins = 0
419
+ for ax, fam in zip(axes.ravel(), order):
420
+ pts = sorted(fams[fam])
421
+ xs = [p[0] for p in pts]
422
+ ys = [p[1] for p in pts]
423
+ deepest = max(xs)
424
+ best_L = max(pts, key=lambda t: t[1])[0]
425
+ win = best_L != deepest
426
+ shallow_wins += win
427
+ c = ROOTC if win else QWENC
428
+ ax.plot(xs, ys, "-", color=c, lw=2, zorder=3, alpha=.9)
429
+ for L, q, fl in pts:
430
+ ax.plot([L], [q], "X" if fl else "o", ms=8.5 if fl else (8 if L == best_L else 6),
431
+ color=DOWN if fl else c, zorder=4, mec="white", mew=1.3)
432
+ ax.axhline(FLOOR_CAL, color=DOWN, ls=(0, (4, 3)), lw=1.1, zorder=1)
433
+ ax.set_title(fam, fontsize=9.5, loc="left", color=INK, pad=4)
434
+ ax.text(.97, .18, "best at %dL" % best_L, transform=ax.transAxes,
435
+ ha="right", va="bottom", fontsize=8.2,
436
+ color=ROOTC if win else MUTED, fontweight="bold" if win else "normal")
437
+ ax.set_xticks([4, 6, 8, 12, 22])
438
+ ax.set_ylim(.468, .566)
439
+ style(ax, xgrid=False, ygrid=True)
440
+ for ax in axes[-1]:
441
+ ax.set_xlabel("layers", fontsize=9)
442
+ for ax in axes[:, 0]:
443
+ ax.set_ylabel("macro F1", fontsize=9)
444
+ fig.suptitle(f"Depth does not reliably buy quality: in {shallow_wins} of {len(order)} encoder "
445
+ f"families the best arm is not the deepest one",
446
+ fontsize=12.5, x=.006, ha="left", y=.978, color=INK)
447
+ handles = [Line2D([], [], color=ROOTC, lw=2.4, label="a shallower variant wins"),
448
+ Line2D([], [], color=QWENC, lw=2.4, label="the deepest variant wins"),
449
+ Line2D([], [], marker="X", ls="", color=DOWN, ms=8,
450
+ label="failed to train: sits exactly on the all-positive floor")]
451
+ fig.legend(handles=handles, fontsize=9, frameon=False, ncol=3,
452
+ loc="lower left", bbox_to_anchor=(.006, .035))
453
+ footer(fig, "Same frozen protocol, three seeds, same split. Each family's own full-depth "
454
+ "checkpoint is the rightmost point.")
455
+ fig.tight_layout(rect=(0, .10, 1, .948))
456
+ fig.savefig(OUT + "16_field_depth_effect.png", dpi=170, facecolor="white")
457
+ plt.close(fig)
458
+
459
+
460
+ def fig_resolution_floor():
461
+ F = sorted(field(), key=lambda r: r["q"])
462
+ med_sd = st.median([r["sd"] for r in F if r["sd"] > 0])
463
+ top = max(r["q"] for r in F)
464
+ band = top - med_sd
465
+ fig, ax = plt.subplots(figsize=(10.4, 9.4))
466
+ fig.patch.set_facecolor("white")
467
+ ax.axvspan(band, top + .004, color=ROOTC, alpha=.08, zorder=1)
468
+ for i, r in enumerate(F):
469
+ c = GRP[r["grp"]][0]
470
+ if r["sd"]:
471
+ ax.plot([r["q"] - r["sd"], r["q"] + r["sd"]], [i, i], color=c, lw=3.4,
472
+ alpha=.30, solid_capstyle="butt", zorder=2)
473
+ if r["floor"]:
474
+ ax.plot([r["q"]], [i], "X", ms=7.5, color=DOWN, zorder=4, mec="white", mew=1.0)
475
+ else:
476
+ ax.plot([r["q"]], [i], "D" if r["grp"] == "root" else "o",
477
+ ms=8 if r["grp"] == "root" else 5.4, color=c,
478
+ zorder=4, mec="white", mew=1.0)
479
+ ax.axvline(FLOOR_CAL, color=DOWN, ls=(0, (5, 3)), lw=1.5, zorder=3)
480
+ ax.text(FLOOR_CAL - .0015, len(F) * .55, f"all-positive floor {FLOOR_CAL:.4f}",
481
+ rotation=90, va="center", ha="right", fontsize=8.6, color=DOWN)
482
+ n_in = sum(1 for r in F if r["q"] >= band)
483
+ ax.set_yticks(range(len(F)))
484
+ ax.set_yticklabels([r["id"].replace("-commonhead", "") for r in F], fontsize=6.9)
485
+ for t, r in zip(ax.get_yticklabels(), F):
486
+ if r["grp"] == "root":
487
+ t.set_fontweight("bold")
488
+ t.set_color(ROOTC)
489
+ ax.set_xlabel("macro F1 (3-seed mean, bar = sample SD)", fontsize=10)
490
+ ax.set_xlim(.466, .607)
491
+ ax.set_ylim(-1.2, len(F) - .3)
492
+ style(ax, xgrid=True)
493
+ ax.spines["left"].set_visible(False)
494
+ ax.set_title("All 65 arms on one axis\n"
495
+ f"shaded band = one median seed-SD ({med_sd:.4f}) below the best arm; "
496
+ f"{n_in} arms fall inside it",
497
+ fontsize=11.5, loc="left", color=INK, pad=12)
498
+ handles = [Line2D([], [], marker="D" if k == "root" else "o", ls="", color=c,
499
+ ms=8 if k == "root" else 6, label=l) for k, (c, l) in GRP.items()]
500
+ handles.append(Line2D([], [], marker="X", ls="", color=DOWN, ms=7.5,
501
+ label="failed to train (on the floor)"))
502
+ ax.legend(handles=handles, fontsize=8.6, frameon=False, loc="upper left",
503
+ bbox_to_anchor=(.012, .995), labelspacing=.5)
504
+ footer(fig, "Three arms — mdeberta-v3-4l, deberta-v3-base-6l, deberta-v3-base-8l — score exactly "
505
+ "the floor. They are reported rather than dropped.")
506
+ fig.tight_layout(rect=(0, .026, 1, 1))
507
+ fig.savefig(OUT + "17_field_resolution_floor.png", dpi=170, facecolor="white")
508
+ plt.close(fig)
509
+
510
+
511
+ if __name__ == "__main__":
512
+ os.makedirs(OUT, exist_ok=True)
513
+ for fn in (fig_calibration_to_test, fig_per_label, fig_seed_spread,
514
+ fig_quality_vs_cost, fig_metric_spans, fig_depth_effect,
515
+ fig_resolution_floor):
516
+ fn()
517
+ print("ok", fn.__name__)