clane9 commited on
Commit
3a5be0d
·
verified ·
1 Parent(s): 9cb061d

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. checkpoints/pretrain_full_90_10_h100/log.json +100 -0
  2. checkpoints/pretrain_full_90_10_h100/log.txt +0 -0
  3. checkpoints/walnut-v0-1/README.md +21 -0
  4. checkpoints/walnut-v0-1/vitb/sub-52k/config.yaml +83 -0
  5. checkpoints/walnut-v0-1/vitl/sub-52k/config.yaml +83 -0
  6. checkpoints/walnut-v0-1/vitl/sub-52k/ddep8/config.yaml +83 -0
  7. finetune/fomo_tune_baseline/build.sh +31 -0
  8. finetune/fomo_tune_baseline/launch.sh +43 -0
  9. finetune/fomo_tune_baseline/output/task1/config.yaml +8 -0
  10. finetune/fomo_tune_baseline/output/task1/log.txt +33 -0
  11. finetune/fomo_tune_baseline/output/task1/metrics.json +1 -0
  12. finetune/fomo_tune_baseline/output/task3/build/Apptainer.def +31 -0
  13. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/README.md +239 -0
  14. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/backbone.py +153 -0
  15. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/datasets.py +205 -0
  16. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task1.py +253 -0
  17. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task3.py +241 -0
  18. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task5.py +245 -0
  19. finetune/fomo_tune_baseline/output/task3/build/fomo_tune/utils.py +33 -0
  20. finetune/fomo_tune_baseline/output/task3/build/model/config.yaml +6 -0
  21. finetune/fomo_tune_baseline/output/task3/build/predict.py +16 -0
  22. finetune/fomo_tune_baseline/output/task3/build/smri_mae/main_pretrain.py +486 -0
  23. finetune/fomo_tune_baseline/output/task3/build/smri_mae/model_mae.py +916 -0
  24. finetune/fomo_tune_baseline/output/task3/config.yaml +6 -0
  25. finetune/fomo_tune_baseline/output/task3/log.txt +30 -0
  26. finetune/fomo_tune_baseline/output/task3/metrics.json +1 -0
  27. finetune/fomo_tune_baseline/output/task3/model/config.yaml +6 -0
  28. finetune/fomo_tune_baseline/output/task5/build/Apptainer.def +31 -0
  29. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/README.md +239 -0
  30. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/backbone.py +153 -0
  31. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/datasets.py +205 -0
  32. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task1.py +253 -0
  33. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task3.py +241 -0
  34. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task5.py +245 -0
  35. finetune/fomo_tune_baseline/output/task5/build/fomo_tune/utils.py +33 -0
  36. finetune/fomo_tune_baseline/output/task5/build/model/config.yaml +6 -0
  37. finetune/fomo_tune_baseline/output/task5/build/predict.py +16 -0
  38. finetune/fomo_tune_baseline/output/task5/build/smri_mae/config/default_pretrain.yaml +98 -0
  39. finetune/fomo_tune_baseline/output/task5/build/smri_mae/main_pretrain.py +486 -0
  40. finetune/fomo_tune_baseline/output/task5/build/smri_mae/masking.py +80 -0
  41. finetune/fomo_tune_baseline/output/task5/build/smri_mae/model_mae.py +916 -0
  42. finetune/fomo_tune_baseline/output/task5/build/smri_mae/modules.py +453 -0
  43. finetune/fomo_tune_baseline/output/task5/build/smri_mae/utils.py +581 -0
  44. finetune/fomo_tune_baseline/output/task5/build/smri_mae/visualization.py +400 -0
  45. finetune/fomo_tune_baseline/output/task5/config.yaml +6 -0
  46. finetune/fomo_tune_baseline/output/task5/log.txt +30 -0
  47. finetune/fomo_tune_baseline/output/task5/metrics.json +1 -0
  48. finetune/fomo_tune_baseline/output/task5/model/config.yaml +6 -0
  49. finetune/fomo_tune_baseline/slurms/build.log +718 -0
  50. finetune/fomo_tune_baseline/slurms/slurm-378508.out +100 -0
checkpoints/pretrain_full_90_10_h100/log.json ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"epoch": 0, "train/lr": 1.25586844637851e-05, "train/grad": 2.920640599727631, "train/loss": 0.7872715425491333, "eval/fomo_val/loss": 0.5462666508161796}
2
+ {"epoch": 1, "train/lr": 3.755973053265827e-05, "train/grad": 1.3527106428146363, "train/loss": 0.5261555862426758, "eval/fomo_val/loss": 0.42567361694461897}
3
+ {"epoch": 2, "train/lr": 6.256077660153144e-05, "train/grad": 0.7325714993476867, "train/loss": 0.46626012682914736, "eval/fomo_val/loss": 0.3726151780137476}
4
+ {"epoch": 3, "train/lr": 8.756182267040462e-05, "train/grad": 0.46042932987213137, "train/loss": 0.4258356285095215, "eval/fomo_val/loss": 0.33146386214022366}
5
+ {"epoch": 4, "train/lr": 0.0001125628687392778, "train/grad": 0.35649376034736635, "train/loss": 0.37943224906921386, "eval/fomo_val/loss": 0.299087589079479}
6
+ {"epoch": 5, "train/lr": 0.00013756391480815097, "train/grad": 0.3172145819664001, "train/loss": 0.3558504378795624, "eval/fomo_val/loss": 0.2806159214028772}
7
+ {"epoch": 6, "train/lr": 0.0001625649608770241, "train/grad": 0.2512174040079117, "train/loss": 0.34055978536605835, "eval/fomo_val/loss": 0.263444991437894}
8
+ {"epoch": 7, "train/lr": 0.00018756600694589727, "train/grad": 0.227120401263237, "train/loss": 0.32977490186691283, "eval/fomo_val/loss": 0.2502139743206636}
9
+ {"epoch": 8, "train/lr": 0.0002125670530147705, "train/grad": 0.2262219125032425, "train/loss": 0.31398472309112546, "eval/fomo_val/loss": 0.24116477066615843}
10
+ {"epoch": 9, "train/lr": 0.00023756768065609442, "train/grad": 0.19973687052726746, "train/loss": 0.31021475315093994, "eval/fomo_val/loss": 0.23339828995038878}
11
+ {"epoch": 10, "train/lr": 0.00024997397135702543, "train/grad": 0.19415394365787506, "train/loss": 0.2942377561330795, "eval/fomo_val/loss": 0.22347858396341216}
12
+ {"epoch": 11, "train/lr": 0.00024982196612385177, "train/grad": 0.18079570591449737, "train/loss": 0.2809535217285156, "eval/fomo_val/loss": 0.21499103560762586}
13
+ {"epoch": 12, "train/lr": 0.000249518493725287, "train/grad": 0.17159102141857147, "train/loss": 0.2636483603715897, "eval/fomo_val/loss": 0.20624621943482813}
14
+ {"epoch": 13, "train/lr": 0.0002490639238957025, "train/grad": 0.16183650910854339, "train/loss": 0.26090574383735654, "eval/fomo_val/loss": 0.20108123466653643}
15
+ {"epoch": 14, "train/lr": 0.0002484588104584144, "train/grad": 0.16673315227031707, "train/loss": 0.2611585813760757, "eval/fomo_val/loss": 0.19515288882660417}
16
+ {"epoch": 15, "train/lr": 0.00024770389065093576, "train/grad": 0.1701878774166107, "train/loss": 0.24610809803009034, "eval/fomo_val/loss": 0.18857810705337885}
17
+ {"epoch": 16, "train/lr": 0.00024680008422676503, "train/grad": 0.15014985650777818, "train/loss": 0.2394261348247528, "eval/fomo_val/loss": 0.18335753156329102}
18
+ {"epoch": 17, "train/lr": 0.0002457484923348096, "train/grad": 0.14950336307287215, "train/loss": 0.2503662085533142, "eval/fomo_val/loss": 0.17991795455509763}
19
+ {"epoch": 18, "train/lr": 0.00024455039617780444, "train/grad": 0.1615193596482277, "train/loss": 0.23103986382484437, "eval/fomo_val/loss": 0.17753474532433275}
20
+ {"epoch": 19, "train/lr": 0.00024320725545136444, "train/grad": 0.14509602308273314, "train/loss": 0.2324778312444687, "eval/fomo_val/loss": 0.17450881507599128}
21
+ {"epoch": 20, "train/lr": 0.00024172070656556968, "train/grad": 0.16689604490995408, "train/loss": 0.22850071251392365, "eval/fomo_val/loss": 0.17120197977659837}
22
+ {"epoch": 21, "train/lr": 0.00024009256065125222, "train/grad": 0.15709128856658935, "train/loss": 0.22197725176811217, "eval/fomo_val/loss": 0.16947166334907965}
23
+ {"epoch": 22, "train/lr": 0.00023832480135341203, "train/grad": 0.1612587684392929, "train/loss": 0.22163174211978912, "eval/fomo_val/loss": 0.16690312148827427}
24
+ {"epoch": 23, "train/lr": 0.00023641958241445106, "train/grad": 0.16475949883461, "train/loss": 0.22064274191856384, "eval/fomo_val/loss": 0.1649576800892938}
25
+ {"epoch": 24, "train/lr": 0.00023437922505016993, "train/grad": 0.1586805135011673, "train/loss": 0.2258121520280838, "eval/fomo_val/loss": 0.16478188597368745}
26
+ {"epoch": 25, "train/lr": 0.00023220621512172403, "train/grad": 0.1567085862159729, "train/loss": 0.21691338539123536, "eval/fomo_val/loss": 0.16126508369760692}
27
+ {"epoch": 26, "train/lr": 0.00022990320010698485, "train/grad": 0.1585501727461815, "train/loss": 0.21671274185180664, "eval/fomo_val/loss": 0.16039053063909964}
28
+ {"epoch": 27, "train/lr": 0.00022747298587499527, "train/grad": 0.15190839976072312, "train/loss": 0.21496180653572083, "eval/fomo_val/loss": 0.15931141266845308}
29
+ {"epoch": 28, "train/lr": 0.00022491853326745144, "train/grad": 0.15507292598485947, "train/loss": 0.21460922420024872, "eval/fomo_val/loss": 0.157214418756512}
30
+ {"epoch": 29, "train/lr": 0.00022224295449137222, "train/grad": 0.16469780147075652, "train/loss": 0.20705958902835847, "eval/fomo_val/loss": 0.1577240099603275}
31
+ {"epoch": 30, "train/lr": 0.00021944950932735515, "train/grad": 0.14905590534210206, "train/loss": 0.20592523217201233, "eval/fomo_val/loss": 0.15570834981945325}
32
+ {"epoch": 31, "train/lr": 0.00021654160115803497, "train/grad": 0.16514489948749542, "train/loss": 0.2095154505968094, "eval/fomo_val/loss": 0.15465696760506}
33
+ {"epoch": 32, "train/lr": 0.0002135227728215875, "train/grad": 0.1505313554406166, "train/loss": 0.21015099346637725, "eval/fomo_val/loss": 0.15362778120445755}
34
+ {"epoch": 33, "train/lr": 0.00021039670229532564, "train/grad": 0.15690734952688218, "train/loss": 0.21043111860752106, "eval/fomo_val/loss": 0.15289746829361286}
35
+ {"epoch": 34, "train/lr": 0.00020716719821465125, "train/grad": 0.14784382790327072, "train/loss": 0.20424923300743103, "eval/fomo_val/loss": 0.1520626851410236}
36
+ {"epoch": 35, "train/lr": 0.00020383819523282006, "train/grad": 0.1598421823978424, "train/loss": 0.2076873528957367, "eval/fomo_val/loss": 0.1513240284796031}
37
+ {"epoch": 36, "train/lr": 0.00020041374922717139, "train/grad": 0.15387804478406905, "train/loss": 0.20155590653419495, "eval/fomo_val/loss": 0.15059169218787608}
38
+ {"epoch": 37, "train/lr": 0.00019689803235766793, "train/grad": 0.1508965849876404, "train/loss": 0.19935197472572327, "eval/fomo_val/loss": 0.14958625525798439}
39
+ {"epoch": 38, "train/lr": 0.0001932953279837605, "train/grad": 0.14703434854745864, "train/loss": 0.20475524961948394, "eval/fomo_val/loss": 0.14901817755879096}
40
+ {"epoch": 39, "train/lr": 0.0001896100254457745, "train/grad": 0.1346307560801506, "train/loss": 0.19926900923252105, "eval/fomo_val/loss": 0.14799074038582027}
41
+ {"epoch": 40, "train/lr": 0.00018584661471717515, "train/grad": 0.14164321571588517, "train/loss": 0.20554547607898713, "eval/fomo_val/loss": 0.14782850649559273}
42
+ {"epoch": 41, "train/lr": 0.00018200968093422642, "train/grad": 0.14752665281295776, "train/loss": 0.19935064673423766, "eval/fomo_val/loss": 0.14740161482455597}
43
+ {"epoch": 42, "train/lr": 0.00017810389880970831, "train/grad": 0.15093021750450134, "train/loss": 0.20380199790000916, "eval/fomo_val/loss": 0.14630420196731136}
44
+ {"epoch": 43, "train/lr": 0.00017413402693750008, "train/grad": 0.15112090498209, "train/loss": 0.19148373246192932, "eval/fomo_val/loss": 0.14608781576718924}
45
+ {"epoch": 44, "train/lr": 0.00017010490199496597, "train/grad": 0.13140210419893264, "train/loss": 0.1884577304124832, "eval/fomo_val/loss": 0.1453945238354071}
46
+ {"epoch": 45, "train/lr": 0.00016602143285020948, "train/grad": 0.13969567716121672, "train/loss": 0.198207226395607, "eval/fomo_val/loss": 0.14440294616627244}
47
+ {"epoch": 46, "train/lr": 0.00016188859458137324, "train/grad": 0.1501631945371628, "train/loss": 0.19735783517360686, "eval/fomo_val/loss": 0.1443709095131676}
48
+ {"epoch": 47, "train/lr": 0.0001577114224152729, "train/grad": 0.1411166822910309, "train/loss": 0.1969533234834671, "eval/fomo_val/loss": 0.14403169548736428}
49
+ {"epoch": 48, "train/lr": 0.00015340826572017794, "train/grad": 0.13484682080646357, "train/loss": 0.19229870662093163, "eval/fomo_val/loss": 0.14312048478508896}
50
+ {"epoch": 49, "train/lr": 0.00014915707728428194, "train/grad": 0.14023476342360178, "train/loss": 0.19414550065994263, "eval/fomo_val/loss": 0.14291474344033114}
51
+ {"epoch": 50, "train/lr": 0.0001448770663438084, "train/grad": 0.14675075436631838, "train/loss": 0.19627596127490202, "eval/fomo_val/loss": 0.14252478741812255}
52
+ {"epoch": 51, "train/lr": 0.00014057344743280325, "train/grad": 0.14737429935485125, "train/loss": 0.19208722313245138, "eval/fomo_val/loss": 0.1418645646212236}
53
+ {"epoch": 52, "train/lr": 0.00013625146384798776, "train/grad": 0.1358876507729292, "train/loss": 0.1927537719408671, "eval/fomo_val/loss": 0.14128415702648883}
54
+ {"epoch": 53, "train/lr": 0.00013191638126060958, "train/grad": 0.12969130308677754, "train/loss": 0.19298341187338033, "eval/fomo_val/loss": 0.14083823333933668}
55
+ {"epoch": 54, "train/lr": 0.00012757348130103307, "train/grad": 0.12538257210204998, "train/loss": 0.1926837439338366, "eval/fomo_val/loss": 0.14034455472005988}
56
+ {"epoch": 55, "train/lr": 0.00012322805512388657, "train/grad": 0.13841872910658518, "train/loss": 0.18828583943347135, "eval/fomo_val/loss": 0.14036549324134612}
57
+ {"epoch": 56, "train/lr": 0.00011888539696160531, "train/grad": 0.12802624329924583, "train/loss": 0.18928068690001965, "eval/fomo_val/loss": 0.1397463431616999}
58
+ {"epoch": 57, "train/lr": 0.00011455079767422488, "train/grad": 0.13150859965632358, "train/loss": 0.1854710274686416, "eval/fomo_val/loss": 0.13939857676906406}
59
+ {"epoch": 58, "train/lr": 0.00011022953830328308, "train/grad": 0.13126523792743683, "train/loss": 0.19039718496302763, "eval/fomo_val/loss": 0.13924091649505327}
60
+ {"epoch": 59, "train/lr": 0.00010592688363768446, "train/grad": 0.14349377558877072, "train/loss": 0.18032679396371046, "eval/fomo_val/loss": 0.1388494759516896}
61
+ {"epoch": 60, "train/lr": 0.00010164807579936582, "train/grad": 0.12802154074112573, "train/loss": 0.1817970940222343, "eval/fomo_val/loss": 0.13819859115020283}
62
+ {"epoch": 61, "train/lr": 9.739832785657841e-05, "train/grad": 0.1388087049126625, "train/loss": 0.18076220775643984, "eval/fomo_val/loss": 0.13775559689076441}
63
+ {"epoch": 62, "train/lr": 9.3182817472567e-05, "train/grad": 0.12375997193157673, "train/loss": 0.1885116429378589, "eval/fomo_val/loss": 0.1378685231197555}
64
+ {"epoch": 63, "train/lr": 8.900668059738495e-05, "train/grad": 0.12581131172676882, "train/loss": 0.18472245956460634, "eval/fomo_val/loss": 0.13720959821399653}
65
+ {"epoch": 64, "train/lr": 8.487500521053002e-05, "train/grad": 0.11724819460262854, "train/loss": 0.18963311364253363, "eval/fomo_val/loss": 0.13685027062892913}
66
+ {"epoch": 65, "train/lr": 8.079282512202535e-05, "train/grad": 0.1248181788250804, "train/loss": 0.1769704738010963, "eval/fomo_val/loss": 0.13661445061553198}
67
+ {"epoch": 66, "train/lr": 7.676511383949712e-05, "train/grad": 0.11684403289109468, "train/loss": 0.1830657379080852, "eval/fomo_val/loss": 0.13631079151945294}
68
+ {"epoch": 67, "train/lr": 7.279677850872176e-05, "train/grad": 0.12013472771892945, "train/loss": 0.18565905342499414, "eval/fomo_val/loss": 0.13585144320748888}
69
+ {"epoch": 68, "train/lr": 6.889265393502457e-05, "train/grad": 0.13088354685654244, "train/loss": 0.18833177971343198, "eval/fomo_val/loss": 0.13563257132498724}
70
+ {"epoch": 69, "train/lr": 6.505749669281434e-05, "train/grad": 0.1235970901325345, "train/loss": 0.18656270764768124, "eval/fomo_val/loss": 0.13527863334934667}
71
+ {"epoch": 70, "train/lr": 6.129597933043005e-05, "train/grad": 0.12204912304878235, "train/loss": 0.18639984975258508, "eval/fomo_val/loss": 0.1351662516875087}
72
+ {"epoch": 71, "train/lr": 5.7612684677360404e-05, "train/grad": 0.12089106285323699, "train/loss": 0.1839581529299418, "eval/fomo_val/loss": 0.13469430298175453}
73
+ {"epoch": 72, "train/lr": 5.401210026077211e-05, "train/grad": 0.12930735231687626, "train/loss": 0.181477606917421, "eval/fomo_val/loss": 0.13472259134616493}
74
+ {"epoch": 73, "train/lr": 5.049861283814926e-05, "train/grad": 0.11069933945933978, "train/loss": 0.1823968899746736, "eval/fomo_val/loss": 0.13440711593852853}
75
+ {"epoch": 74, "train/lr": 4.70765030527051e-05, "train/grad": 0.10915143756816785, "train/loss": 0.17597144097089767, "eval/fomo_val/loss": 0.13389452583384964}
76
+ {"epoch": 75, "train/lr": 4.374994021807759e-05, "train/grad": 0.11530119584252436, "train/loss": 0.1775289699435234, "eval/fomo_val/loss": 0.13390670171323812}
77
+ {"epoch": 76, "train/lr": 4.052297723866298e-05, "train/grad": 0.1125206748644511, "train/loss": 0.18334729907413325, "eval/fomo_val/loss": 0.13359323290159117}
78
+ {"epoch": 77, "train/lr": 3.7399545671776155e-05, "train/grad": 0.11088902006546657, "train/loss": 0.179920989399155, "eval/fomo_val/loss": 0.1332294022418418}
79
+ {"epoch": 78, "train/lr": 3.438345093765363e-05, "train/grad": 0.11161495838314295, "train/loss": 0.17800653353333473, "eval/fomo_val/loss": 0.1330351065633432}
80
+ {"epoch": 79, "train/lr": 3.147836768313514e-05, "train/grad": 0.10522990642736356, "train/loss": 0.17757239130636057, "eval/fomo_val/loss": 0.1331123783340994}
81
+ {"epoch": 80, "train/lr": 2.868783530467256e-05, "train/grad": 0.09980769486476977, "train/loss": 0.17774887258807817, "eval/fomo_val/loss": 0.13267944176804344}
82
+ {"epoch": 81, "train/lr": 2.6015253636120466e-05, "train/grad": 0.10013275531431039, "train/loss": 0.17619845954080424, "eval/fomo_val/loss": 0.13248468544685615}
83
+ {"epoch": 82, "train/lr": 2.3463878806562344e-05, "train/grad": 0.10081137996166945, "train/loss": 0.1800100685407718, "eval/fomo_val/loss": 0.13219575589557864}
84
+ {"epoch": 83, "train/lr": 2.1036819273218863e-05, "train/grad": 0.10347515996545553, "train/loss": 0.17568077395359674, "eval/fomo_val/loss": 0.13195786242777446}
85
+ {"epoch": 84, "train/lr": 1.8737032034271527e-05, "train/grad": 0.10440930351614952, "train/loss": 0.17420727759599686, "eval/fomo_val/loss": 0.13185373426608318}
86
+ {"epoch": 85, "train/lr": 1.656731902621594e-05, "train/grad": 0.10837028237680595, "train/loss": 0.18037758581340313, "eval/fomo_val/loss": 0.13179254700552742}
87
+ {"epoch": 86, "train/lr": 1.453032371013373e-05, "train/grad": 0.09870245152463515, "train/loss": 0.1831672458599011, "eval/fomo_val/loss": 0.131568816950861}
88
+ {"epoch": 87, "train/lr": 1.2628527851042481e-05, "train/grad": 0.08623698353767395, "train/loss": 0.171711598833402, "eval/fomo_val/loss": 0.1315375879688083}
89
+ {"epoch": 88, "train/lr": 1.0864248494247304e-05, "train/grad": 0.09079501374314229, "train/loss": 0.17652994766831398, "eval/fomo_val/loss": 0.13138468673206724}
90
+ {"epoch": 89, "train/lr": 9.239635142378045e-06, "train/grad": 0.08837371009091537, "train/loss": 0.1798645822952191, "eval/fomo_val/loss": 0.13128385861527245}
91
+ {"epoch": 90, "train/lr": 7.756667136551455e-06, "train/grad": 0.096140431240201, "train/loss": 0.1783239816625913, "eval/fomo_val/loss": 0.13115769554421586}
92
+ {"epoch": 91, "train/lr": 6.41715124484893e-06, "train/grad": 0.08655263110995293, "train/loss": 0.17633002437651157, "eval/fomo_val/loss": 0.1311222384560783}
93
+ {"epoch": 92, "train/lr": 5.222719461047898e-06, "train/grad": 0.08044332855691512, "train/loss": 0.1724255457520485, "eval/fomo_val/loss": 0.13100630379510375}
94
+ {"epoch": 93, "train/lr": 4.174827016288814e-06, "train/grad": 0.0795227784352998, "train/loss": 0.17683392018079758, "eval/fomo_val/loss": 0.13084294014381911}
95
+ {"epoch": 94, "train/lr": 3.2747506061001546e-06, "train/grad": 0.0814427686855197, "train/loss": 0.18071980526049933, "eval/fomo_val/loss": 0.13090927108841122}
96
+ {"epoch": 95, "train/lr": 2.5235868349415963e-06, "train/grad": 0.08623279072344303, "train/loss": 0.1820303381731113, "eval/fomo_val/loss": 0.1308269995563435}
97
+ {"epoch": 96, "train/lr": 1.922250880160383e-06, "train/grad": 0.07390511563668649, "train/loss": 0.16746863164007664, "eval/fomo_val/loss": 0.13083390045278478}
98
+ {"epoch": 97, "train/lr": 1.4714753769887204e-06, "train/grad": 0.07653172205512722, "train/loss": 0.17233146354556084, "eval/fomo_val/loss": 0.1307800050895169}
99
+ {"epoch": 98, "train/lr": 1.1718095259405694e-06, "train/grad": 0.07844715456788738, "train/loss": 0.17596244563659033, "eval/fomo_val/loss": 0.13069988840593483}
100
+ {"epoch": 99, "train/lr": 1.0236184236954529e-06, "train/grad": 0.07888089073821902, "train/loss": 0.18072639467815557, "eval/fomo_val/loss": 0.13072822161440578}
checkpoints/pretrain_full_90_10_h100/log.txt ADDED
The diff for this file is too large to render. See raw diff
 
checkpoints/walnut-v0-1/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: pytorch
3
+ tags:
4
+ - medical-imaging
5
+ - vision-transformer
6
+ - masked-autoencoder
7
+ - mri
8
+ ---
9
+
10
+ # Walnut v0.1
11
+
12
+ Pretrained 3D ViT-MAE checkpoints for structural MRI.
13
+
14
+ The `vitb` and `vitl` directories contain models trained with subject-specific
15
+ sampling at four dataset scales. These runs used a fixed compute budget of
16
+ 99,000 optimizer steps with global batch size 256. Each directory contains the
17
+ final checkpoint and its training configuration.
18
+
19
+ `vitl/sub-52k/ddep8` is the corresponding full-subject ViT-L experiment with an
20
+ eight-layer decoder.
21
+
checkpoints/walnut-v0-1/vitb/sub-52k/config.yaml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: scale_subjects_vitb_n52643_resume_v1_mb64_gbs256_s7338
2
+ notes: unique_subject_scaling_resume_v1_microbatch64
3
+ output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v5_resumed/scale_subjects_vitb_n52643_resume_v1_mb64_gbs256_s7338
4
+ img_size:
5
+ - 208
6
+ - 240
7
+ - 208
8
+ patch_size: 8
9
+ mask_ratio: 0.8
10
+ pred_mask_ratio: null
11
+ pad_to_multiple: 32
12
+ masking_strategy: random
13
+ block_mask_fraction: 0.7
14
+ block_mask_min_size: 2
15
+ block_mask_max_size: 6
16
+ model: mae_vit_base
17
+ model_kwargs:
18
+ target_norm: none
19
+ no_decode_pos: false
20
+ mask_drop_scale: false
21
+ class_token: true
22
+ reg_tokens: 0
23
+ no_embed_class: false
24
+ decoder_depth: 4
25
+ drop_path_rate: 0.0
26
+ datasets:
27
+ fomo_train:
28
+ url: /data/smri-datasets/FOMO300/train/shard.*.tar
29
+ subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
30
+ subject_manifest: null
31
+ subject_count: 52643
32
+ subset_seed: 7338
33
+ include_modalities: null
34
+ samples_per_epoch: 253440
35
+ read_ahead: 2048
36
+ drop_last: true
37
+ fomo_val:
38
+ url: /data/smri-datasets/FOMO300/val/shard.*.tar
39
+ samples_per_epoch: 29278
40
+ shuffle: false
41
+ buffer_size: 0
42
+ drop_last: false
43
+ train_dataset: fomo_train
44
+ eval_datasets:
45
+ - fomo_val
46
+ num_workers: 4
47
+ prefetch_factor: 2
48
+ presend_cuda: true
49
+ epochs: 100
50
+ batch_size: 64
51
+ accum_iter: 1
52
+ base_lr: 0.001
53
+ min_lr: 1.0e-06
54
+ warmup_epochs: 10
55
+ weight_decay: 0.05
56
+ betas:
57
+ - 0.9
58
+ - 0.95
59
+ clip_grad: 1.0
60
+ amp: true
61
+ amp_dtype: bfloat16
62
+ ckpt: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v1/scale_subjects_vitb_n52643_steps99000_s7338/checkpoint-last.pth
63
+ resume: true
64
+ auto_resume: false
65
+ start_epoch: 0
66
+ checkpoint_period: 10
67
+ max_checkpoints: 0
68
+ eval_period: 10
69
+ r2_sync: null
70
+ device: cuda
71
+ distributed: true
72
+ seed: 7338
73
+ data_seed: 7338
74
+ eval_seed: 7338
75
+ debug: false
76
+ wandb: true
77
+ wandb_entity: null
78
+ wandb_project: smri-fm
79
+ lr: null
80
+ rank: 0
81
+ world_size: 4
82
+ gpu: 0
83
+ dist_backend: nccl
checkpoints/walnut-v0-1/vitl/sub-52k/config.yaml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: scale_subjects_vitl_n52643_resume_v1_mb64_gbs256_s7338
2
+ notes: unique_subject_scaling_resume_v1_microbatch64
3
+ output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v5_resumed/scale_subjects_vitl_n52643_resume_v1_mb64_gbs256_s7338
4
+ img_size:
5
+ - 208
6
+ - 240
7
+ - 208
8
+ patch_size: 8
9
+ mask_ratio: 0.8
10
+ pred_mask_ratio: null
11
+ pad_to_multiple: 32
12
+ masking_strategy: random
13
+ block_mask_fraction: 0.7
14
+ block_mask_min_size: 2
15
+ block_mask_max_size: 6
16
+ model: mae_vit_large
17
+ model_kwargs:
18
+ target_norm: none
19
+ no_decode_pos: false
20
+ mask_drop_scale: false
21
+ class_token: true
22
+ reg_tokens: 0
23
+ no_embed_class: false
24
+ decoder_depth: 4
25
+ drop_path_rate: 0.0
26
+ datasets:
27
+ fomo_train:
28
+ url: /data/smri-datasets/FOMO300/train/shard.*.tar
29
+ subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
30
+ subject_manifest: null
31
+ subject_count: 52643
32
+ subset_seed: 7338
33
+ include_modalities: null
34
+ samples_per_epoch: 253440
35
+ read_ahead: 2048
36
+ drop_last: true
37
+ fomo_val:
38
+ url: /data/smri-datasets/FOMO300/val/shard.*.tar
39
+ samples_per_epoch: 29278
40
+ shuffle: false
41
+ buffer_size: 0
42
+ drop_last: false
43
+ train_dataset: fomo_train
44
+ eval_datasets:
45
+ - fomo_val
46
+ num_workers: 4
47
+ prefetch_factor: 2
48
+ presend_cuda: true
49
+ epochs: 100
50
+ batch_size: 64
51
+ accum_iter: 1
52
+ base_lr: 0.001
53
+ min_lr: 1.0e-06
54
+ warmup_epochs: 10
55
+ weight_decay: 0.05
56
+ betas:
57
+ - 0.9
58
+ - 0.95
59
+ clip_grad: 1.0
60
+ amp: true
61
+ amp_dtype: bfloat16
62
+ ckpt: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v1/scale_subjects_vitl_n52643_steps99000_s7338/checkpoint-last.pth
63
+ resume: true
64
+ auto_resume: false
65
+ start_epoch: 0
66
+ checkpoint_period: 10
67
+ max_checkpoints: 0
68
+ eval_period: 10
69
+ r2_sync: null
70
+ device: cuda
71
+ distributed: true
72
+ seed: 7338
73
+ data_seed: 7338
74
+ eval_seed: 7338
75
+ debug: false
76
+ wandb: true
77
+ wandb_entity: null
78
+ wandb_project: smri-fm
79
+ lr: null
80
+ rank: 0
81
+ world_size: 4
82
+ gpu: 0
83
+ dist_backend: nccl
checkpoints/walnut-v0-1/vitl/sub-52k/ddep8/config.yaml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: vitl_n52k_ddep8
2
+ notes: vitl_full_subjects_mae_decoder_depth8_fixed_compute
3
+ output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/vitl_n52k_ddep8
4
+ img_size:
5
+ - 208
6
+ - 240
7
+ - 208
8
+ patch_size: 8
9
+ mask_ratio: 0.8
10
+ pred_mask_ratio: null
11
+ pad_to_multiple: 32
12
+ masking_strategy: random
13
+ block_mask_fraction: 0.7
14
+ block_mask_min_size: 2
15
+ block_mask_max_size: 6
16
+ model: mae_vit_large
17
+ model_kwargs:
18
+ target_norm: none
19
+ no_decode_pos: false
20
+ mask_drop_scale: false
21
+ class_token: true
22
+ reg_tokens: 0
23
+ no_embed_class: false
24
+ decoder_depth: 8
25
+ drop_path_rate: 0.0
26
+ datasets:
27
+ fomo_train:
28
+ url: /data/smri-datasets/FOMO300/train/shard.*.tar
29
+ subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
30
+ subject_manifest: null
31
+ subject_count: 52643
32
+ subset_seed: 7338
33
+ include_modalities: null
34
+ samples_per_epoch: 253440
35
+ read_ahead: 2048
36
+ drop_last: true
37
+ fomo_val:
38
+ url: /data/smri-datasets/FOMO300/val/shard.*.tar
39
+ samples_per_epoch: 29278
40
+ shuffle: false
41
+ buffer_size: 0
42
+ drop_last: false
43
+ train_dataset: fomo_train
44
+ eval_datasets:
45
+ - fomo_val
46
+ num_workers: 4
47
+ prefetch_factor: 2
48
+ presend_cuda: true
49
+ epochs: 100
50
+ batch_size: 32
51
+ accum_iter: 2
52
+ base_lr: 0.001
53
+ min_lr: 1.0e-06
54
+ warmup_epochs: 10
55
+ weight_decay: 0.05
56
+ betas:
57
+ - 0.9
58
+ - 0.95
59
+ clip_grad: 1.0
60
+ amp: true
61
+ amp_dtype: bfloat16
62
+ ckpt: null
63
+ resume: false
64
+ auto_resume: true
65
+ start_epoch: 0
66
+ checkpoint_period: 10
67
+ max_checkpoints: 0
68
+ eval_period: 10
69
+ r2_sync: null
70
+ device: cuda
71
+ distributed: true
72
+ seed: 7338
73
+ data_seed: 7338
74
+ eval_seed: 7338
75
+ debug: false
76
+ wandb: true
77
+ wandb_entity: null
78
+ wandb_project: smri-fm
79
+ lr: null
80
+ rank: 0
81
+ world_size: 4
82
+ gpu: 0
83
+ dist_backend: nccl
finetune/fomo_tune_baseline/build.sh ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Package each trained run into its challenge .sif. Run on the login node, after launch.sh:
3
+ # apptainer lives there and on n-6 only, and a build needs no GPU driver.
4
+ #
5
+ # Slow. Apptainer always re-runs %post, so each of the three re-downloads ~3G of wheels.
6
+
7
+ set -euo pipefail
8
+
9
+ ROOT="/data/connor/nanobrain.1"
10
+ cd $ROOT
11
+
12
+ EXP_DIR="experiments/fomo_tune_baseline"
13
+ OUT_DIR="${EXP_DIR}/output"
14
+
15
+ runs=(task1 task5 task3)
16
+
17
+ for name in "${runs[@]}"; do
18
+ # build.py names the sif after `task` in the run's saved config, which is the run name here
19
+ sif="${OUT_DIR}/${name}/${name}.sif"
20
+
21
+ if [[ -f "${sif}" ]]; then
22
+ echo "sif ${sif} exists; skipping"
23
+ continue
24
+ fi
25
+
26
+ echo "=== ${name} ==="
27
+ uv run --no-sync python -m fomo_tune.build "${OUT_DIR}/${name}"
28
+ done
29
+
30
+ echo "=== sifs ==="
31
+ ls -lh "${OUT_DIR}"/*/*.sif
finetune/fomo_tune_baseline/launch.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #SBATCH --job-name=fomo_tune
3
+ #SBATCH --nodes=1
4
+ #SBATCH --ntasks-per-node=1
5
+ #SBATCH --gpus-per-task=1
6
+ #SBATCH --time=2:00:00
7
+ #SBATCH --partition=main
8
+ #SBATCH --output=slurms/slurm-%j.out
9
+ #SBATCH --account=sophont
10
+
11
+ set -euo pipefail
12
+
13
+ ROOT="/data/connor/nanobrain.1"
14
+ cd $ROOT
15
+
16
+ EXP_DIR="experiments/fomo_tune_baseline"
17
+ OUT_DIR="${EXP_DIR}/output"
18
+
19
+ # name, module. Cheapest first, so a broken environment fails in 90s.
20
+ # One job, run sequentially: the whole grid is ~12 minutes and each task loads the same
21
+ # 3.9G checkpoint, so an array would only add queue time and cache contention.
22
+ runs=(
23
+ "task1 main_task1"
24
+ "task5 main_task5"
25
+ "task3 main_task3"
26
+ )
27
+
28
+ for run in "${runs[@]}"; do
29
+ read -r name module <<<"${run}"
30
+
31
+ if [[ -f "${OUT_DIR}/${name}/metrics.json" ]]; then
32
+ echo "result ${name} exists; skipping"
33
+ continue
34
+ fi
35
+
36
+ echo "=== ${name} ==="
37
+ uv run --no-sync python -m "fomo_tune.${module}" train \
38
+ output_root="${OUT_DIR}" \
39
+ name="${name}"
40
+ done
41
+
42
+ echo "=== results ==="
43
+ cat "${OUT_DIR}"/*/metrics.json
finetune/fomo_tune_baseline/output/task1/config.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ task: task1
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ modalities:
4
+ - dwi_b1000
5
+ output_root: experiments/fomo_tune_baseline/output
6
+ name: task1
7
+ device: cuda
8
+ seed: 4466
finetune/fomo_tune_baseline/output/task1/log.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 16:31:38 run task1 (git 1df2e5d)
2
+ 16:31:38 config:
3
+ task: task1
4
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
5
+ modalities:
6
+ - dwi_b1000
7
+ output_root: experiments/fomo_tune_baseline/output
8
+ name: task1
9
+ device: cuda
10
+ seed: 4466
11
+ 16:31:40 dataset: 21 subjects, 13 positive
12
+ 16:31:48 fold 1/21 sub-01 y=1 p=0.512 (4s)
13
+ 16:31:49 fold 2/21 sub-02 y=1 p=0.514 (4s)
14
+ 16:31:49 fold 3/21 sub-03 y=0 p=0.486 (5s)
15
+ 16:31:49 fold 4/21 sub-04 y=1 p=0.499 (5s)
16
+ 16:31:50 fold 5/21 sub-05 y=1 p=0.507 (5s)
17
+ 16:31:50 fold 6/21 sub-06 y=1 p=0.493 (6s)
18
+ 16:31:50 fold 7/21 sub-07 y=0 p=0.489 (6s)
19
+ 16:31:51 fold 8/21 sub-08 y=0 p=0.490 (6s)
20
+ 16:31:51 fold 9/21 sub-09 y=1 p=0.523 (7s)
21
+ 16:31:52 fold 10/21 sub-10 y=1 p=0.525 (7s)
22
+ 16:31:52 fold 11/21 sub-11 y=0 p=0.479 (7s)
23
+ 16:31:52 fold 12/21 sub-12 y=0 p=0.474 (8s)
24
+ 16:31:53 fold 13/21 sub-13 y=1 p=0.516 (8s)
25
+ 16:31:53 fold 14/21 sub-14 y=1 p=0.536 (9s)
26
+ 16:31:53 fold 15/21 sub-15 y=0 p=0.486 (9s)
27
+ 16:31:54 fold 16/21 sub-16 y=0 p=0.482 (9s)
28
+ 16:31:54 fold 17/21 sub-17 y=1 p=0.520 (10s)
29
+ 16:31:54 fold 18/21 sub-18 y=1 p=0.518 (10s)
30
+ 16:31:55 fold 19/21 sub-19 y=1 p=0.520 (10s)
31
+ 16:31:55 fold 20/21 sub-20 y=1 p=0.519 (11s)
32
+ 16:31:56 fold 21/21 sub-21 y=0 p=0.498 (11s)
33
+ 16:31:57 result: auroc=0.9904 auroc_ci_low=0.9444 auroc_ci_high=1.0000 (11s)
finetune/fomo_tune_baseline/output/task1/metrics.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name": "task1", "auroc": 0.9903846153846154, "auroc_ci_low": 0.9444444444444445, "auroc_ci_high": 1.0, "run_time": 11.2}
finetune/fomo_tune_baseline/output/task3/build/Apptainer.def ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Bootstrap: docker
2
+ From: python:3.11-slim
3
+
4
+ # NOT buildable where it sits: the %files paths below are relative to the build cwd, which is the
5
+ # staging dir `build.py` writes. Build it with `python -m fomo_tune.build <run_dir>`, not by
6
+ # pointing apptainer at this file.
7
+ #
8
+ # Versions are pinned to the training environment: numpy, scikit-learn and joblib because they
9
+ # unpickle `head.joblib`, torch because that is what the checkpoint was written by.
10
+
11
+ %files
12
+ fomo_tune /app/fomo_tune
13
+ smri_mae /app/smri_mae
14
+ model /app/model
15
+ predict.py /app/predict.py
16
+
17
+ %post
18
+ pip install --no-cache-dir \
19
+ torch==2.8.0 \
20
+ numpy==2.4.6 \
21
+ nibabel==5.4.2 \
22
+ einops==0.8.2 \
23
+ jaxtyping==0.3.10 \
24
+ timm==1.0.27 \
25
+ huggingface-hub==0.36.2 \
26
+ scikit-learn==1.8.0 \
27
+ joblib==1.5.3 \
28
+ omegaconf==2.3.0
29
+
30
+ %runscript
31
+ exec python /app/predict.py "$@"
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/README.md ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fomo_tune
2
+
3
+ The five FOMO26 challenge tasks, one script each, tuned independently.
4
+
5
+ This is a spinoff of `nanobrain.eval`, which scored every backbone on every task through one fixed
6
+ probe. That was the right shape for a benchmark and the wrong shape for a competition: here we care
7
+ about one backbone (sMRI MAE) and five scores, and each task wants a different method. **Nothing
8
+ here imports `nanobrain.eval`, and it should stay that way** — this package may be shared with
9
+ people who won't get the eval suite.
10
+
11
+ ## Layout
12
+
13
+ | File | |
14
+ |---|---|
15
+ | `datasets.py` | core, **frozen**. One `load_fomo_task<k>()` per task, streaming the challenge zips into an HF dataset. Raw niftis, no resampling — the backbone transform does that. |
16
+ | `backbone.py` | core, **frozen**. `load_backbone(ckpt_path) -> (SmriMaeBackbone, SmriMaeTransform)`. Frozen sMRI MAE encoder; the transform canonicalizes to RAS, rescales to 1mm, fits to the pretraining shape, z-scores in a mean-threshold brain mask. |
17
+ | `utils.py` | core. `set_seed`, `git_sha`, `setup_logging`. |
18
+ | `main_task<k>.py` | shell. One task, end to end. Task 1 is the worked example; copy it. |
19
+ | `build.py` + `Apptainer.def` | shell. Package a run dir into the challenge `.sif`. Shared by every task. |
20
+
21
+ `datasets.py` and `backbone.py` are settled and their caches are warm. Treat them as read-only:
22
+ new work goes in `main_task<k>.py`. If one of them genuinely needs to change, that is a
23
+ conversation first, because it invalidates every score already recorded.
24
+
25
+ ## The pattern
26
+
27
+ `main_task1.py` is in three sections, and the split is the point of the whole design.
28
+
29
+ **`Task1Method` — the part we tune.** Features, head, hyperparameters, anything that might move
30
+ the score. Its interface is:
31
+
32
+ ```python
33
+ method.fit(rows) # rows are dataset records: subject, label, images
34
+ method.predict(images) # -> the challenge's output for one subject
35
+ method.save(model_dir) # config.yaml + head.joblib
36
+ Task1Method.load(model_dir, **overrides)
37
+ ```
38
+
39
+ **The protocol — fixed.** Pool out-of-fold predictions over all subjects, bootstrap subjects for
40
+ the CI. No repeats, no stratification; the bootstrap is the only variance estimate. Splitting is
41
+ per-task but fixed within a task — leave-one-out where n is tiny (task 1, n=21), **20-fold** where
42
+ it isn't (tasks 3 and 5), which is close enough to LOO without paying for 494 refits. Once a task's
43
+ scheme is set, hold it or scores stop being comparable across iterations. That is also why
44
+ `cross_validate` seeds its shuffle at 0 rather than from `cfg.seed`: the folds are part of the
45
+ protocol, so tuning the run's seed must not silently redraw them.
46
+
47
+ **Two entrypoints.** `train` runs the protocol then fits a head on all subjects and saves it;
48
+ `predict` is the challenge CLI. Both go through `Method.predict`, which is why every fold
49
+ exercises the code the submission will run.
50
+
51
+ That last point is the load-bearing one. `predict` is not a wrapper written at packaging time — it
52
+ is the same call cross-validation already made once per held-out subject. When you add a task, keep
53
+ that property.
54
+
55
+ ```bash
56
+ uv run python -m fomo_tune.main_task1 train modalities=[dwi_b1000,flair] name=task1_dwi_flair
57
+ uv run python -m fomo_tune.main_task1 predict --model-dir output/fomo_tune/task1_dwi/model \
58
+ --adc adc.nii.gz --dwi dwi.nii.gz --flair flair.nii.gz --output prob.txt
59
+ ```
60
+
61
+ `train` takes omegaconf dotlist overrides against the `Config` dataclass at the top of the file.
62
+ It writes `config.yaml`, `log.txt`, `metrics.json`, and `model/` into `{output_root}/{name}/`.
63
+
64
+ ## Status
65
+
66
+ Tasks 1, 5 and 3 are drafted and verified. Task 1 is also packaged — its container passes the
67
+ challenge validator; 5 and 3 have not been built yet. **Tasks 2 and 4 are tabled** — both are
68
+ segmentation, both need `predict` to emit a nifti on the input grid, and neither is worth opening
69
+ until the classification and regression tasks are settled.
70
+
71
+ All three on `vitl_fomo300`, one H100, wall being the cross-validation loop:
72
+
73
+ | run | result | wall |
74
+ |---|---|---|
75
+ | `task1_dwi`, dwi_b1000, n=21, LOO | AUROC **0.990** [0.944, 1.000] | 25s |
76
+ | `task5_t1w`, t1w, n=48, 20-fold | AUROC **0.984** [0.953, 1.000] | 73s |
77
+ | `task3_t1w`, t1w, n=494, 20-fold | r **0.962** [0.956, 0.968], MAE **3.71y** [3.45, 3.97] | 260s |
78
+
79
+ **Task 3's row is one fold-seed stale.** It was measured before `cross_validate` froze its shuffle
80
+ at 0, so it is a 20-fold run with `random_state=4466`. Task 1 (LOO) and task 5 are current. The
81
+ re-run is cheap — 260s on a GPU — it just has not been done. Expect a shift of the same order task
82
+ 5 saw when its folds moved (0.948 → 0.984, i.e. inside the CI but not negligible).
83
+
84
+ Task 1's earlier probe sweep got 0.954 [0.861, 1.000] on the same checkpoint
85
+ (`experiments/eval_global_0728`), so it roughly reproduces — the gap is LOO vs 5×5 stratified CV,
86
+ one interpolation instead of two, and a head selected on AUROC instead of balanced accuracy.
87
+
88
+ Two checks worth repeating per task — `.claude/scratch/verify_task1.py` and
89
+ `.claude/scratch/verify_task35.py <k>` do both:
90
+ - features are **bit-identical** whether the nifti comes from the HF dataset wrapper or from
91
+ `nib.load` off disk, so CV numbers transfer to the container
92
+ - the `predict` CLI agrees with the in-process method
93
+
94
+ ## What changes per task
95
+
96
+ Counts and modalities, read from the local zips:
97
+
98
+ | Task | n | Inputs | Output | Split | Notes |
99
+ |---|---|---|---|---|---|
100
+ | 1 infarct | 21 | adc, dwi_b1000, flair (+t2s/swi) | probability | LOO | done |
101
+ | 5 polymicrogyria | 48 | t1w | probability | 20-fold | done |
102
+ | 3 brain age | 494 | t1w | age in years | 20-fold | done — RidgeCV head, scored by **Pearson r and MAE**, each with its own bootstrap CI |
103
+ | 2 meningioma | 23 | dwi_b1000, flair (+t2s/swi) | mask, input grid | — | tabled |
104
+ | 4 trigeminal | 40 | t2w | mask, labels 1=nerve 2=vessel | — | tabled |
105
+
106
+ Tasks 5 and 3 diverge from task 1 only where that table says. `cross_validate` over a shuffled
107
+ `KFold` replaces `leave_one_out`; both take one modality, so `features` loses the
108
+ concat-over-modalities loop and `Config` loses `modalities`; the challenge CLI flag is `--t1` for
109
+ both, and it is `--t1` for task 3 too even though the file in the zip is `t1w.nii.gz`.
110
+
111
+ Task 3 is the first regression, so its `score` loops over the two metrics rather than returning
112
+ one, and it drops task 1's guard against bootstrap resamples with fewer than two distinct labels.
113
+ The analogous degenerate case for regression is a resample with no spread in `y`, where Pearson r
114
+ is undefined rather than merely unstable — at n=494 it does not happen.
115
+
116
+ When tasks 2 and 4 come back: `predict` must write a nifti on the input's grid, and the method
117
+ needs localized features rather than a pooled vector — `backbone.forward` returns `patch_coords`
118
+ in world mm for exactly that. Task 4's label order (1=nerve, 2=vessel) is still a guess and needs
119
+ confirming against the challenge data before per-class numbers mean anything.
120
+
121
+ ## Gotchas
122
+
123
+ **Raw niftis are on disk** at `data/fomo_eval/Task_<k>/preprocessed/<sub>/ses-01/`, which is the
124
+ easy way to exercise `predict` on a real file rather than one written out of the dataset:
125
+
126
+ ```bash
127
+ uv run python -m fomo_tune.main_task1 predict \
128
+ --model-dir output/fomo_tune/task1_dwi/model \
129
+ --adc data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/adc.nii.gz \
130
+ --dwi data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/dwi_b1000.nii.gz \
131
+ --flair data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/flair.nii.gz \
132
+ --output /tmp/prob.txt
133
+ ```
134
+
135
+ Task 5 breaks the naming: `Task_5/preprocessed/sub_01/ses_01/t1.nii.gz` — underscores throughout,
136
+ and `t1` not `t1w`. `datasets.py` already handles it; anything you write by hand won't.
137
+
138
+ ```bash
139
+ uv run python -m fomo_tune.main_task5 predict --model-dir output/fomo_tune/task5_t1w/model \
140
+ --t1 data/fomo_eval/Task_5/preprocessed/sub_01/ses_01/t1.nii.gz --output /tmp/prob.txt
141
+ ```
142
+
143
+ **Volumes are wildly anisotropic.** Task 1's DWI is 0.46×0.46×**5.6**mm, so the transform
144
+ upsamples z by 5.6× to reach 1mm iso. Nothing is wrong, but don't read the 1mm grid as real
145
+ resolution.
146
+
147
+ **The backbone never saw skull or neck.** Pretraining used a SynthSeg brain mask; the transform
148
+ substitutes a mean-intensity threshold, which keeps both. Known fidelity gap — see
149
+ `.claude/memory/smri-mae-preprocessing-gap.md`.
150
+
151
+ **Probabilities are not calibrated.** `LogisticRegressionCV` on ~20 samples × 1024 features shrinks
152
+ hard; task 1's out-of-fold probabilities all land in 0.48–0.52 with near-perfect ranking. Fine for
153
+ AUROC, which is what the challenge scores, but don't read them as probabilities. Task 5's do span
154
+ 0–1, which is n=48 rather than n=21 and not evidence of calibration.
155
+
156
+ **n is tiny, so the CI is the result.** Task 1's is ~0.06 wide at the top of the range. Most tuning
157
+ deltas you chase will be inside it. `.claude/NOTES.md` thread 1 has the longer argument.
158
+
159
+ **GPUs need an allocation** — the login node has no driver. See the `gpu-session` skill.
160
+
161
+ ## Submission
162
+
163
+ `build.py` packages a run dir into the `.sif` the challenge wants. One command, taking the run dir
164
+ the shipped head was saved into:
165
+
166
+ ```bash
167
+ uv run python -m fomo_tune.build output/fomo_tune/task1_dwi
168
+ ```
169
+
170
+ It stages `/app`, then builds from there:
171
+
172
+ ```
173
+ /app/predict.py # shim: calls fomo_tune.main_task<k> predict
174
+ /app/model/config.yaml # from the run dir
175
+ /app/model/head.joblib # from the run dir
176
+ /app/model/backbone.pth # stripped checkpoint, --ckpt-path points here
177
+ ```
178
+
179
+ **Both `build.py` and `Apptainer.def` are shared across tasks**, which they can be because nothing
180
+ in staging or in the dependency list is task-specific. The one thing that does vary is the module
181
+ the shim imports, and that comes from `task` in the run's saved config — so a run dir knows which
182
+ task it belongs to, and `build.py` never needs telling.
183
+
184
+ `predict.py` is **generated at build time** rather than checked in. It is eight lines whose whole
185
+ meaning is the container layout staged around it, so there is nowhere outside a container to run
186
+ it. This does not weaken the point above about `predict` not being written at packaging time: the
187
+ logic still lives in `main_task<k>.py`, exercised once per fold, and the shim only picks the
188
+ subcommand and two paths.
189
+
190
+ **`Apptainer.def` is not buildable where it sits.** Its `%files` paths are relative to the build
191
+ cwd, which is the staging dir. Pointing `apptainer build` at it in the repo fails confusingly; go
192
+ through `build.py`.
193
+
194
+ The run dir deliberately does *not* carry backbone weights — that checkpoint is 3.9G and would be
195
+ copied on every run. `--ckpt-path` overrides what `config.yaml` recorded, so the saved config stays
196
+ a faithful record of what trained rather than being rewritten at build time.
197
+
198
+ **The staged checkpoint is stripped to `model` and `args`**, which is 3.9G → 1.3G because the rest
199
+ is optimizer state inference never reads. `load_backbone` needs no change for this, and `predict`
200
+ gives a bit-identical probability either way (0.524739 on `sub-20`, checked on GPU).
201
+
202
+ **The base image is `python:3.11-slim`, not a CUDA image.** The PyPI torch wheel *is* the cu128
203
+ build and vendors the whole CUDA userspace as `nvidia-*` packages, so all the container needs from
204
+ the host is the driver, which `--nv`/`--nvccli` binds in. That keeps the SIF at 5.3G (4.0G of
205
+ image, 1.3G of weights) against roughly double for `pytorch/pytorch` and far more for NGC.
206
+ Versions are pinned to the training environment
207
+ mostly so `head.joblib` unpickles against the numpy/sklearn that wrote it.
208
+
209
+ Apptainer caches the bootstrap layers but **always re-runs `%post`**, so every build re-downloads
210
+ ~3G of wheels. If that gets annoying, bake a deps-only base SIF and `Bootstrap: localimage` off it.
211
+
212
+ ### Validating
213
+
214
+ `third_party/container-validator` is the challenge's own validator, test niftis included:
215
+
216
+ ```bash
217
+ python third_party/container-validator/container_validator/validate.py \
218
+ --task task1 --sif output/fomo_tune/task1_dwi/task1.sif
219
+ ```
220
+
221
+ It runs `python /app/predict.py --flair /input/… --adc … --dwi … --swi … --output /output/<sid>.txt`
222
+ inside an `apptainer instance` with `/input`, `/output` and `/tmp` bound — which is exactly the
223
+ shim's contract, so nothing in `predict.py` is guessing at the interface.
224
+
225
+ One thing it does that is easy to miss: it takes GPU via `--nvccli` rather than `--nv`, and one of
226
+ its tests runs `nvidia-smi -L` **inside** the container. `python:3.11-slim` ships no `nvidia-smi`,
227
+ so that test passes only because `--nvccli` injects the host one — a CUDA base image would hide
228
+ that dependency rather than remove it.
229
+
230
+ **The `task1_dwi` container passes all 20 validator tests**, and `predict` inside it returns
231
+ 0.524739 on `sub-20`, identical to the same call outside the container. So the packaging is
232
+ verified end to end, not just built.
233
+
234
+ **Run it on a compute node with apptainer, which as of 2026-08-11 means `n-6`** — `salloc
235
+ --nodelist=n-6`. The other nodes fail the validator's preflight. The login node has apptainer but
236
+ no driver, and
237
+ `predict` there dies inside `can_use_cudnn_attention` — the jagged-SDPA path reaches into CUDA even
238
+ when the tensors are on CPU, so a driver-less host fails at the forward pass rather than falling
239
+ back. That is the CPU gap worth remembering; it is not a container problem.
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/backbone.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ import nibabel as nib
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from einops import rearrange
9
+ from torch import Tensor
10
+
11
+ import smri_mae.model_mae as models_mae
12
+
13
+
14
+ class SmriMaeBackbone(nn.Module):
15
+ grid_coords: Tensor
16
+
17
+ def __init__(self, encoder: models_mae.MaskedEncoder):
18
+ super().__init__()
19
+ self.encoder = encoder
20
+ self.img_size = self.encoder.patchify.img_size
21
+
22
+ grid_size = self.encoder.patchify.grid_size
23
+ patch_size = np.array(self.encoder.patchify.patch_size)
24
+ grid_coords = rearrange(np.indices(grid_size), "c x y z -> (x y z) c")
25
+ grid_coords = grid_coords * patch_size + (patch_size - 1) / 2
26
+ grid_coords = torch.as_tensor(grid_coords, dtype=torch.float32)
27
+ self.register_buffer("grid_coords", grid_coords)
28
+
29
+ def forward(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
30
+ images = batch["image"]
31
+ mask = batch["mask"]
32
+ affine = batch["affine"]
33
+
34
+ B, C, X, Y, Z = images.shape
35
+ assert (X, Y, Z) == self.img_size, f"expected {self.img_size}, got {(X, Y, Z)}"
36
+
37
+ _, _, patch_embeds, _, patch_ids, token_mask = self.encoder(images, mask=mask)
38
+
39
+ # [B, L, 3] world xyz coords of embeddings
40
+ patch_coords = self.grid_coords[patch_ids, :]
41
+ rot = affine[:, :3, :3]
42
+ trans = affine[:, :3, 3]
43
+ patch_coords = patch_coords @ rot.transpose(1, 2) + trans[:, None, :]
44
+
45
+ return {
46
+ "patch_embeds": patch_embeds,
47
+ "patch_ids": patch_ids,
48
+ "token_mask": token_mask,
49
+ "patch_coords": patch_coords,
50
+ }
51
+
52
+
53
+ class SmriMaeTransform:
54
+ def __init__(
55
+ self,
56
+ img_size: tuple[int, int, int] = (208, 240, 208),
57
+ spacing: tuple[float, float, float] = (1.0, 1.0, 1.0),
58
+ ):
59
+ self.img_size = img_size
60
+ self.spacing = spacing
61
+
62
+ def __call__(self, img: nib.Nifti1Image) -> dict[str, Tensor]:
63
+ # repack image to handle incomplete hf Nifti interface
64
+ img = nib.Nifti1Image(img.dataobj, img.affine, img.header)
65
+ img = nib.as_closest_canonical(img)
66
+
67
+ data = torch.from_numpy(np.ascontiguousarray(img.get_fdata(dtype=np.float32)))
68
+ affine = np.asarray(img.affine)
69
+
70
+ spacing = img.header.get_zooms()
71
+ if max(abs(s - s_) for s, s_ in zip(spacing, self.spacing)) > 0.05:
72
+ data, affine = rescale(data, affine, spacing, self.spacing)
73
+
74
+ data, affine = fit_to_shape(data, affine, target_shape=self.img_size)
75
+
76
+ # mean threshold, not the SynthSeg mask used in pretraining, so skull and neck stay in
77
+ mask = data > data.mean()
78
+ brain = data[mask]
79
+ mean = brain.mean()
80
+ # population std (correction=0) to match the pretraining normalization
81
+ std = brain.std(correction=0).clamp_min(1e-6)
82
+ data = torch.where(mask, (data - mean) / std, 0.0)
83
+
84
+ return {
85
+ "image": data.unsqueeze(0),
86
+ "mask": mask.unsqueeze(0),
87
+ "affine": torch.as_tensor(affine, dtype=torch.float32),
88
+ }
89
+
90
+
91
+ def rescale(
92
+ x: torch.Tensor,
93
+ affine: np.ndarray,
94
+ spacing: tuple[float, ...],
95
+ target_spacing: tuple[float, ...] = (1.0, 1.0, 1.0),
96
+ ) -> tuple[torch.Tensor, np.ndarray]:
97
+ scales = tuple([current / target for current, target in zip(spacing, target_spacing)])
98
+ resampled = F.interpolate(x[None, None], scale_factor=scales, mode="trilinear").squeeze(0, 1)
99
+
100
+ # align_corners=False reads output voxel j from input voxel (j + 0.5) / scale - 0.5
101
+ scale = np.asarray(scales, dtype=float)
102
+ step = np.diag([*(1 / scale), 1.0])
103
+ step[:3, 3] = 0.5 / scale - 0.5
104
+ return resampled, affine @ step
105
+
106
+
107
+ def fit_to_shape(
108
+ x: torch.Tensor, affine: np.ndarray, target_shape: tuple[int, ...]
109
+ ) -> tuple[torch.Tensor, np.ndarray]:
110
+ """Centre the volume in `target_shape`, padding the short axes and cropping the long ones."""
111
+ pads = [target - size for size, target in zip(x.shape, target_shape)]
112
+ padding = [side for pad in reversed(pads) for side in (pad // 2, pad - pad // 2)]
113
+
114
+ # a crop is a negative pad, so output voxel k came from input voxel k - pad // 2 either way
115
+ step = np.eye(4)
116
+ step[:3, 3] = [-(pad // 2) for pad in pads]
117
+ return F.pad(x, padding), affine @ step
118
+
119
+
120
+ def resolve_ckpt(ckpt_path: str) -> str:
121
+ """A local path for a checkpoint, downloading it if it is an hf://<org>/<repo>/<file> URI."""
122
+ from huggingface_hub import hf_hub_download
123
+
124
+ if ckpt_path.startswith("hf://"):
125
+ org, repo, *rest = ckpt_path.removeprefix("hf://").split("/")
126
+ return hf_hub_download(f"{org}/{repo}", "/".join(rest))
127
+
128
+ return ckpt_path
129
+
130
+
131
+ def load_backbone(ckpt_path: str) -> tuple[SmriMaeBackbone, SmriMaeTransform]:
132
+ path = resolve_ckpt(ckpt_path)
133
+ ckpt = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
134
+ args = ckpt["args"]
135
+
136
+ model_fn = models_mae.__dict__[args["model"]]
137
+ model: models_mae.MaskedAutoencoderViT = model_fn(
138
+ img_size=args["img_size"],
139
+ in_chans=args.get("in_chans", 1),
140
+ patch_size=args["patch_size"],
141
+ # older checkpoints carry training flags the current model_mae no longer takes
142
+ **filter_kwargs(models_mae.MaskedAutoencoderViT, args.get("model_kwargs") or {}),
143
+ )
144
+ model.load_state_dict(ckpt["model"])
145
+ backbone = SmriMaeBackbone(model.encoder)
146
+ transform = SmriMaeTransform(img_size=args["img_size"])
147
+ return backbone, transform
148
+
149
+
150
+ def filter_kwargs(func, kwargs):
151
+ signature = inspect.signature(func)
152
+ kwargs = {k: v for k, v in kwargs.items() if k in signature.parameters}
153
+ return kwargs
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/datasets.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ import zipfile
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+
9
+ import fsspec
10
+ from datasets import Dataset, Features, Nifti, Value
11
+
12
+ FOMO_EVAL_BASE_URL = os.getenv(
13
+ "FOMO_EVAL_BASE_URL",
14
+ "https://sid.erda.dk/share_redirect/fmeuvo1EdF",
15
+ )
16
+ FOMO_EVAL_TASK5_URL = os.getenv(
17
+ "FOMO_EVAL_TASK5_URL",
18
+ "https://huggingface.co/datasets/medarc/smri-fm/resolve/main/fomo_eval/Task_5.zip",
19
+ )
20
+
21
+
22
+ @contextmanager
23
+ def open_zip(url: str) -> Generator[zipfile.ZipFile, None, None]:
24
+ """Open a task zip, copying a remote url to a temp file first."""
25
+ with tempfile.TemporaryDirectory() as tmp:
26
+ local = Path(url)
27
+ if not local.exists():
28
+ local = Path(tmp) / "task.zip"
29
+ with fsspec.open(url) as src, local.open("wb") as dst:
30
+ shutil.copyfileobj(src, dst)
31
+ with zipfile.ZipFile(local) as zf:
32
+ yield zf
33
+
34
+
35
+ def subject_ids(zf: zipfile.ZipFile) -> list[str]:
36
+ return sorted({name.split("/")[2] for name in zf.namelist() if name.endswith(".nii.gz")})
37
+
38
+
39
+ # ---- Task 1: acute infarct (classification; positives also carry a lesion mask) --------
40
+
41
+
42
+ def load_fomo_task1() -> Dataset:
43
+ # No 4th modality: it is swi on 16 subjects and t2s on the other 5.
44
+ suffixes = ("adc", "dwi_b1000", "flair")
45
+ features = Features(
46
+ {
47
+ "subject": Value("string"),
48
+ "label": Value("int32"),
49
+ **{suffix: Nifti() for suffix in suffixes},
50
+ }
51
+ )
52
+ dataset = Dataset.from_generator(
53
+ _fomo_task1_generator,
54
+ features=features,
55
+ gen_kwargs={"suffixes": suffixes},
56
+ writer_batch_size=16,
57
+ )
58
+ return dataset
59
+
60
+
61
+ def _fomo_task1_generator(suffixes: tuple[str, ...]):
62
+ url = f"{FOMO_EVAL_BASE_URL}/Task_1.zip"
63
+ with open_zip(url) as zf:
64
+ for sub in subject_ids(zf):
65
+ label = int(zf.read(f"Task_1/labels/{sub}/ses-01/label.txt").strip())
66
+ sample = {"subject": sub, "label": label}
67
+ for suffix in suffixes:
68
+ name = f"Task_1/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
69
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
70
+ yield sample
71
+
72
+
73
+ # ---- Task 2: meningioma segmentation ---------------------------------------------------
74
+
75
+
76
+ def load_fomo_task2() -> Dataset:
77
+ # No 4th modality: it is t2s on 15 subjects and swi on the other 8.
78
+ suffixes = ("dwi_b1000", "flair")
79
+ features = Features(
80
+ {
81
+ "subject": Value("string"),
82
+ **{suffix: Nifti() for suffix in suffixes},
83
+ "seg": Nifti(),
84
+ }
85
+ )
86
+ dataset = Dataset.from_generator(
87
+ _fomo_task2_generator,
88
+ features=features,
89
+ gen_kwargs={"suffixes": suffixes},
90
+ writer_batch_size=16,
91
+ )
92
+ return dataset
93
+
94
+
95
+ def _fomo_task2_generator(suffixes: tuple[str, ...]):
96
+ url = f"{FOMO_EVAL_BASE_URL}/Task_2.zip"
97
+ with open_zip(url) as zf:
98
+ for sub in subject_ids(zf):
99
+ sample = {"subject": sub}
100
+ for suffix in suffixes:
101
+ name = f"Task_2/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
102
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
103
+ # Seg is on the image grid (shapes match) but its affine differs by up to 0.03mm.
104
+ name = f"Task_2/labels/{sub}/ses-01/seg.nii.gz"
105
+ sample["seg"] = {"path": None, "bytes": zf.read(name)}
106
+ yield sample
107
+
108
+
109
+ # ---- Task 3: brain age regression ------------------------------------------------------
110
+
111
+
112
+ def load_fomo_task3() -> Dataset:
113
+ features = Features(
114
+ {
115
+ "subject": Value("string"),
116
+ "age": Value("int32"),
117
+ "t1w": Nifti(),
118
+ }
119
+ )
120
+ dataset = Dataset.from_generator(
121
+ _fomo_task3_generator,
122
+ features=features,
123
+ writer_batch_size=16,
124
+ )
125
+ return dataset
126
+
127
+
128
+ def _fomo_task3_generator():
129
+ url = f"{FOMO_EVAL_BASE_URL}/Task_3.zip"
130
+ with open_zip(url) as zf:
131
+ for sub in subject_ids(zf):
132
+ age = int(zf.read(f"Task_3/labels/{sub}/ses-01/labels.txt").strip())
133
+ image_gz = zf.read(f"Task_3/preprocessed/{sub}/ses-01/t1w.nii.gz")
134
+ sample = {
135
+ "subject": sub,
136
+ "age": age,
137
+ "t1w": {"path": None, "bytes": image_gz},
138
+ }
139
+ yield sample
140
+
141
+
142
+ # ---- Task 4: trigeminal nerve/vessel segmentation --------------------------------------
143
+
144
+
145
+ def load_fomo_task4() -> Dataset:
146
+ # Volumes are uncropped 0.5mm near-iso, ~360x512x512; crop before feeding a model.
147
+ features = Features(
148
+ {
149
+ "subject": Value("string"),
150
+ "t2w": Nifti(),
151
+ "seg": Nifti(),
152
+ }
153
+ )
154
+ dataset = Dataset.from_generator(
155
+ _fomo_task4_generator,
156
+ features=features,
157
+ writer_batch_size=16,
158
+ )
159
+ return dataset
160
+
161
+
162
+ def _fomo_task4_generator():
163
+ url = f"{FOMO_EVAL_BASE_URL}/Task_4.zip"
164
+ with open_zip(url) as zf:
165
+ for sub in subject_ids(zf):
166
+ image_gz = zf.read(f"Task_4/preprocessed/{sub}/ses-01/t2w.nii.gz")
167
+ seg_gz = zf.read(f"Task_4/labels/{sub}/ses-01/seg.nii.gz")
168
+ sample = {
169
+ "subject": sub,
170
+ "t2w": {"path": None, "bytes": image_gz},
171
+ "seg": {"path": None, "bytes": seg_gz},
172
+ }
173
+ yield sample
174
+
175
+
176
+ # ---- Task 5: polymicrogyria classification ---------------------------------------------
177
+
178
+
179
+ def load_fomo_task5() -> Dataset:
180
+ features = Features(
181
+ {
182
+ "subject": Value("string"),
183
+ "label": Value("int32"),
184
+ "t1w": Nifti(),
185
+ }
186
+ )
187
+ dataset = Dataset.from_generator(
188
+ _fomo_task5_generator,
189
+ features=features,
190
+ writer_batch_size=16,
191
+ )
192
+ return dataset
193
+
194
+
195
+ def _fomo_task5_generator():
196
+ with open_zip(FOMO_EVAL_TASK5_URL) as zf:
197
+ for sub in subject_ids(zf):
198
+ label = int(zf.read(f"Task_5/labels/{sub}/ses_01/labels.txt").strip())
199
+ image_gz = zf.read(f"Task_5/preprocessed/{sub}/ses_01/t1.nii.gz")
200
+ sample = {
201
+ "subject": sub,
202
+ "label": label,
203
+ "t1w": {"path": None, "bytes": image_gz},
204
+ }
205
+ yield sample
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task1.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 1: acute infarct classification, scored by AUROC as the challenge scores it.
2
+
3
+ `Task1Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: leave one subject out, pool the out-of-fold
5
+ predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract,
8
+ modality paths in and one probability out. Both go through `Task1Method.predict`, so every fold
9
+ exercises the path the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import LogisticRegressionCV
25
+ from sklearn.metrics import roc_auc_score
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task1"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ modalities: list[str] = field(default_factory=lambda: ["dwi_b1000"])
44
+ output_root: str = "output/fomo_tune"
45
+ name: str = "task1"
46
+ device: str = "cuda"
47
+ seed: int = 4466
48
+
49
+
50
+ # ---- method: the part we tune -----------------------------------------------------------
51
+
52
+
53
+ class Task1Method:
54
+ """Frozen sMRI MAE, mean-pooled tokens per modality concatenated, logistic head."""
55
+
56
+ def __init__(self, cfg: Config):
57
+ self.cfg = cfg
58
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
59
+ self.device = torch.device(cfg.device)
60
+ self.backbone.to(self.device).eval().requires_grad_(False)
61
+ self.modalities = list(cfg.modalities)
62
+ self.cache: dict[str, np.ndarray] = {}
63
+ self.head = None
64
+
65
+ @torch.inference_mode()
66
+ def features(self, images: Images) -> np.ndarray:
67
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
68
+ pooled = []
69
+ for modality in self.modalities:
70
+ sample = self.transform(images[modality])
71
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
72
+
73
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
74
+ out = self.backbone(batch)
75
+
76
+ patch_embeds = out["patch_embeds"]
77
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
78
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
79
+ pooled.append(embed[0].float().cpu())
80
+
81
+ return torch.cat(pooled).numpy()
82
+
83
+ def cached_features(self, row: dict) -> np.ndarray:
84
+ if row["subject"] not in self.cache:
85
+ self.cache[row["subject"]] = self.features(row)
86
+ return self.cache[row["subject"]]
87
+
88
+ def fit(self, rows: list[dict]) -> None:
89
+ X = np.stack([self.cached_features(row) for row in rows])
90
+ y = np.array([row["label"] for row in rows])
91
+
92
+ clf = LogisticRegressionCV(
93
+ Cs=10,
94
+ class_weight="balanced",
95
+ scoring="roc_auc",
96
+ max_iter=1000,
97
+ l1_ratios=(0,),
98
+ use_legacy_attributes=False,
99
+ )
100
+ self.head = make_pipeline(StandardScaler(), clf)
101
+ self.head.fit(X, y)
102
+ self.positive = list(self.head.classes_).index(1)
103
+
104
+ def predict(self, images: Images) -> float:
105
+ """Positive-class probability. Indexes `classes_` rather than assuming column 1, which
106
+ would silently score the wrong class if the label order differed."""
107
+ X = self.features(images)[None]
108
+ probs = self.head.predict_proba(X)[0]
109
+ return float(probs[self.positive])
110
+
111
+ def save(self, model_dir: Path) -> None:
112
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
113
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
114
+ model_dir.mkdir(parents=True, exist_ok=True)
115
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
116
+ joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
117
+
118
+ @classmethod
119
+ def load(cls, model_dir: Path, **overrides) -> "Task1Method":
120
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
121
+ between here and the container -- the backbone path, the device."""
122
+ cfg = OmegaConf.merge(
123
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
124
+ )
125
+ method = cls(cfg)
126
+ state = joblib.load(model_dir / "head.joblib")
127
+ method.head, method.positive = state["head"], state["positive"]
128
+ return method
129
+
130
+
131
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
132
+
133
+ # Every image the task ships. The method picks which of them it wants, as at inference, where
134
+ # the challenge hands over all four modalities whether or not a model uses them.
135
+ IMAGE_COLS = ("adc", "dwi_b1000", "flair")
136
+
137
+
138
+ def leave_one_out(rows: list[dict], method: Task1Method) -> tuple[np.ndarray, np.ndarray]:
139
+ """Out-of-fold score for every subject, each predicted by a head fit on the other n-1."""
140
+ y = np.array([row["label"] for row in rows])
141
+ oof = np.zeros(len(rows), dtype=float)
142
+ start = time.perf_counter()
143
+ for held_out, row in enumerate(rows):
144
+ method.fit([r for r in rows if r["subject"] != row["subject"]])
145
+ oof[held_out] = method.predict({key: row[key] for key in IMAGE_COLS})
146
+ logger.info(
147
+ f"fold {held_out + 1}/{len(rows)} {row['subject']} "
148
+ f"y={y[held_out]} p={oof[held_out]:.3f} ({time.perf_counter() - start:.0f}s)"
149
+ )
150
+ return y, oof
151
+
152
+
153
+ def score(
154
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
155
+ ) -> dict:
156
+ """AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
157
+ rng = np.random.default_rng(seed)
158
+ samples = []
159
+ for _ in range(n_boot):
160
+ rows = rng.integers(0, len(y), size=len(y))
161
+ if len(np.unique(y[rows])) < 2:
162
+ continue
163
+ samples.append(roc_auc_score(y[rows], oof[rows]))
164
+
165
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
166
+ return {
167
+ "auroc": float(roc_auc_score(y, oof)),
168
+ "auroc_ci_low": float(low),
169
+ "auroc_ci_high": float(high),
170
+ }
171
+
172
+
173
+ # ---- entrypoints ------------------------------------------------------------------------
174
+
175
+
176
+ def train(args: argparse.Namespace) -> None:
177
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
178
+ from fomo_tune.datasets import load_fomo_task1
179
+
180
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
181
+ run_dir = Path(cfg.output_root) / cfg.name
182
+ run_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ setup_logging(run_dir)
185
+ set_seed(cfg.seed)
186
+ logger.info(f"run {cfg.name} (git {git_sha()})")
187
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
188
+ OmegaConf.save(cfg, run_dir / "config.yaml")
189
+
190
+ # decoded once: leave-one-out revisits every subject n times, and the niftis are small
191
+ rows = list(load_fomo_task1())
192
+ logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
193
+
194
+ method = Task1Method(cfg)
195
+ start = time.perf_counter()
196
+ y, oof = leave_one_out(rows, method)
197
+ run_time = time.perf_counter() - start
198
+ summary = score(y, oof)
199
+
200
+ # the shipped head sees all n subjects, so it is not any of the models scored above
201
+ method.fit(rows)
202
+ method.save(run_dir / "model")
203
+
204
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
205
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
206
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
207
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
208
+
209
+
210
+ def predict(args: argparse.Namespace) -> None:
211
+ """The challenge contract: modality paths in, one probability written to `--output`.
212
+
213
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
214
+ code leave-one-out already ran, not something generated at build time.
215
+ """
216
+ overrides = {"device": args.device}
217
+ if args.ckpt_path:
218
+ overrides["ckpt_path"] = args.ckpt_path
219
+ method = Task1Method.load(args.model_dir, **overrides)
220
+
221
+ # every image the challenge hands over, as in `leave_one_out`; the method takes what it uses
222
+ paths = {"adc": args.adc, "dwi_b1000": args.dwi, "flair": args.flair}
223
+ probability = method.predict({key: nib.load(path) for key, path in paths.items()})
224
+
225
+ args.output.write_text(f"{probability:.6f}\n")
226
+
227
+
228
+ def main() -> None:
229
+ parser = argparse.ArgumentParser()
230
+ modes = parser.add_subparsers(required=True)
231
+
232
+ train_parser = modes.add_parser("train", help="leave-one-out over the task, then fit and save")
233
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
234
+ train_parser.set_defaults(run=train)
235
+
236
+ predict_parser = modes.add_parser("predict", help="one subject, one probability")
237
+ for flag in ("--flair", "--adc", "--dwi"):
238
+ predict_parser.add_argument(flag, type=Path, required=True)
239
+ # accepted and ignored: the 4th modality is swi on some subjects and t2s on others
240
+ for flag in ("--t2s", "--swi"):
241
+ predict_parser.add_argument(flag, type=Path)
242
+ predict_parser.add_argument("--output", type=Path, required=True)
243
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
244
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
245
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
246
+ predict_parser.set_defaults(run=predict)
247
+
248
+ args = parser.parse_args()
249
+ args.run(args)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task3.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 3: brain age regression, scored by Pearson r and MAE as the challenge scores it.
2
+
3
+ `Task3Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: 20-fold over the 494 subjects, pool the
5
+ out-of-fold predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract, one t1
8
+ path in and one age out. Both go through `Task3Method.predict`, so every fold exercises the path
9
+ the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import RidgeCV
25
+ from sklearn.model_selection import KFold
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task3"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ output_root: str = "output/fomo_tune"
44
+ name: str = "task3"
45
+ device: str = "cuda"
46
+ seed: int = 4466
47
+
48
+
49
+ # ---- method: the part we tune -----------------------------------------------------------
50
+
51
+
52
+ class Task3Method:
53
+ """Frozen sMRI MAE, mean-pooled tokens over the t1w, ridge head."""
54
+
55
+ def __init__(self, cfg: Config):
56
+ self.cfg = cfg
57
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
58
+ self.device = torch.device(cfg.device)
59
+ self.backbone.to(self.device).eval().requires_grad_(False)
60
+ self.cache: dict[str, np.ndarray] = {}
61
+ self.head = None
62
+
63
+ @torch.inference_mode()
64
+ def features(self, images: Images) -> np.ndarray:
65
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
66
+ sample = self.transform(images["t1w"])
67
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
68
+
69
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
70
+ out = self.backbone(batch)
71
+
72
+ patch_embeds = out["patch_embeds"]
73
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
74
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
75
+ return embed[0].float().cpu().numpy()
76
+
77
+ def cached_features(self, row: dict) -> np.ndarray:
78
+ if row["subject"] not in self.cache:
79
+ self.cache[row["subject"]] = self.features(row)
80
+ return self.cache[row["subject"]]
81
+
82
+ def fit(self, rows: list[dict]) -> None:
83
+ X = np.stack([self.cached_features(row) for row in rows])
84
+ y = np.array([row["age"] for row in rows], dtype=float)
85
+
86
+ # RidgeCV picks alpha by its own efficient leave-one-out, so the fold's own split is
87
+ # never touched by model selection
88
+ self.head = make_pipeline(StandardScaler(), RidgeCV(alphas=np.logspace(-3, 6, 19)))
89
+ self.head.fit(X, y)
90
+
91
+ def predict(self, images: Images) -> float:
92
+ """Age in years."""
93
+ X = self.features(images)[None]
94
+ return float(self.head.predict(X)[0])
95
+
96
+ def save(self, model_dir: Path) -> None:
97
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
98
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
99
+ model_dir.mkdir(parents=True, exist_ok=True)
100
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
101
+ joblib.dump(self.head, model_dir / "head.joblib")
102
+
103
+ @classmethod
104
+ def load(cls, model_dir: Path, **overrides) -> "Task3Method":
105
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
106
+ between here and the container -- the backbone path, the device."""
107
+ cfg = OmegaConf.merge(
108
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
109
+ )
110
+ method = cls(cfg)
111
+ method.head = joblib.load(model_dir / "head.joblib")
112
+ return method
113
+
114
+
115
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
116
+
117
+ # Every image the task ships. The method picks which of them it wants, as at inference, where the
118
+ # challenge hands over the modalities whether or not a model uses them.
119
+ IMAGE_COLS = ("t1w",)
120
+
121
+
122
+ def cross_validate(
123
+ rows: list[dict], method: Task3Method, seed: int = 0, n_folds: int = 20
124
+ ) -> tuple[np.ndarray, np.ndarray]:
125
+ """Out-of-fold age for every subject, each predicted by a head fit on the other folds."""
126
+ y = np.array([row["age"] for row in rows], dtype=float)
127
+ oof = np.zeros(len(rows), dtype=float)
128
+ folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
129
+ start = time.perf_counter()
130
+ for fold, (train, test) in enumerate(folds.split(rows)):
131
+ method.fit([rows[i] for i in train])
132
+ for i in test:
133
+ oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
134
+ logger.info(
135
+ f"fold {fold + 1}/{n_folds} n={len(test)} mae={np.abs(y[test] - oof[test]).mean():.2f} "
136
+ f"({time.perf_counter() - start:.0f}s)"
137
+ )
138
+ return y, oof
139
+
140
+
141
+ def metrics(y: np.ndarray, oof: np.ndarray) -> dict:
142
+ return {
143
+ "pearson_r": float(np.corrcoef(y, oof)[0, 1]),
144
+ "mae": float(np.abs(y - oof).mean()),
145
+ }
146
+
147
+
148
+ def score(
149
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
150
+ ) -> dict:
151
+ """Both challenge metrics, each with a percentile CI resampling subjects with replacement."""
152
+ rng = np.random.default_rng(seed)
153
+ resamples = rng.integers(0, len(y), size=(n_boot, len(y)))
154
+
155
+ summary = {}
156
+ for name, point in metrics(y, oof).items():
157
+ samples = [metrics(y[rows], oof[rows])[name] for rows in resamples]
158
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
159
+ summary[name] = point
160
+ summary[f"{name}_ci_low"] = float(low)
161
+ summary[f"{name}_ci_high"] = float(high)
162
+ return summary
163
+
164
+
165
+ # ---- entrypoints ------------------------------------------------------------------------
166
+
167
+
168
+ def train(args: argparse.Namespace) -> None:
169
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
170
+ from fomo_tune.datasets import load_fomo_task3
171
+
172
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
173
+ run_dir = Path(cfg.output_root) / cfg.name
174
+ run_dir.mkdir(parents=True, exist_ok=True)
175
+
176
+ setup_logging(run_dir)
177
+ set_seed(cfg.seed)
178
+ logger.info(f"run {cfg.name} (git {git_sha()})")
179
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
180
+ OmegaConf.save(cfg, run_dir / "config.yaml")
181
+
182
+ rows = list(load_fomo_task3())
183
+ ages = np.array([row["age"] for row in rows])
184
+ logger.info(
185
+ f"dataset: {len(rows)} subjects, age {ages.min()}-{ages.max()} mean {ages.mean():.1f}"
186
+ )
187
+
188
+ method = Task3Method(cfg)
189
+ start = time.perf_counter()
190
+ y, oof = cross_validate(rows, method)
191
+ run_time = time.perf_counter() - start
192
+ summary = score(y, oof)
193
+
194
+ # the shipped head sees all n subjects, so it is not any of the models scored above
195
+ method.fit(rows)
196
+ method.save(run_dir / "model")
197
+
198
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
199
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
200
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
201
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
202
+
203
+
204
+ def predict(args: argparse.Namespace) -> None:
205
+ """The challenge contract: a t1 path in, one age written to `--output`.
206
+
207
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
208
+ code cross-validation already ran, not something generated at build time.
209
+ """
210
+ overrides = {"device": args.device}
211
+ if args.ckpt_path:
212
+ overrides["ckpt_path"] = args.ckpt_path
213
+ method = Task3Method.load(args.model_dir, **overrides)
214
+
215
+ age = method.predict({"t1w": nib.load(args.t1)})
216
+
217
+ args.output.write_text(f"{age:.6f}\n")
218
+
219
+
220
+ def main() -> None:
221
+ parser = argparse.ArgumentParser()
222
+ modes = parser.add_subparsers(required=True)
223
+
224
+ train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
225
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
226
+ train_parser.set_defaults(run=train)
227
+
228
+ predict_parser = modes.add_parser("predict", help="one subject, one age in years")
229
+ predict_parser.add_argument("--t1", type=Path, required=True)
230
+ predict_parser.add_argument("--output", type=Path, required=True)
231
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
232
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
233
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
234
+ predict_parser.set_defaults(run=predict)
235
+
236
+ args = parser.parse_args()
237
+ args.run(args)
238
+
239
+
240
+ if __name__ == "__main__":
241
+ main()
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task5.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 5: polymicrogyria classification, scored by AUROC as the challenge scores it.
2
+
3
+ `Task5Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: 20-fold over the 48 subjects, pool the
5
+ out-of-fold predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract, one t1
8
+ path in and one probability out. Both go through `Task5Method.predict`, so every fold exercises
9
+ the path the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import LogisticRegressionCV
25
+ from sklearn.metrics import roc_auc_score
26
+ from sklearn.model_selection import KFold
27
+ from sklearn.pipeline import make_pipeline
28
+ from sklearn.preprocessing import StandardScaler
29
+
30
+ from fomo_tune.backbone import load_backbone
31
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
32
+
33
+ logger = logging.getLogger("fomo_tune")
34
+
35
+ Images = dict[str, nib.Nifti1Image]
36
+
37
+
38
+ @dataclass
39
+ class Config:
40
+ task: str = "task5"
41
+ ckpt_path: str = (
42
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
43
+ )
44
+ output_root: str = "output/fomo_tune"
45
+ name: str = "task5"
46
+ device: str = "cuda"
47
+ seed: int = 4466
48
+
49
+
50
+ # ---- method: the part we tune -----------------------------------------------------------
51
+
52
+
53
+ class Task5Method:
54
+ """Frozen sMRI MAE, mean-pooled tokens over the t1w, logistic head."""
55
+
56
+ def __init__(self, cfg: Config):
57
+ self.cfg = cfg
58
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
59
+ self.device = torch.device(cfg.device)
60
+ self.backbone.to(self.device).eval().requires_grad_(False)
61
+ self.cache: dict[str, np.ndarray] = {}
62
+ self.head = None
63
+
64
+ @torch.inference_mode()
65
+ def features(self, images: Images) -> np.ndarray:
66
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
67
+ sample = self.transform(images["t1w"])
68
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
69
+
70
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
71
+ out = self.backbone(batch)
72
+
73
+ patch_embeds = out["patch_embeds"]
74
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
75
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
76
+ return embed[0].float().cpu().numpy()
77
+
78
+ def cached_features(self, row: dict) -> np.ndarray:
79
+ if row["subject"] not in self.cache:
80
+ self.cache[row["subject"]] = self.features(row)
81
+ return self.cache[row["subject"]]
82
+
83
+ def fit(self, rows: list[dict]) -> None:
84
+ X = np.stack([self.cached_features(row) for row in rows])
85
+ y = np.array([row["label"] for row in rows])
86
+
87
+ clf = LogisticRegressionCV(
88
+ Cs=10,
89
+ class_weight="balanced",
90
+ scoring="roc_auc",
91
+ max_iter=1000,
92
+ l1_ratios=(0,),
93
+ use_legacy_attributes=False,
94
+ )
95
+ self.head = make_pipeline(StandardScaler(), clf)
96
+ self.head.fit(X, y)
97
+ self.positive = list(self.head.classes_).index(1)
98
+
99
+ def predict(self, images: Images) -> float:
100
+ """Positive-class probability. Indexes `classes_` rather than assuming column 1, which
101
+ would silently score the wrong class if the label order differed."""
102
+ X = self.features(images)[None]
103
+ probs = self.head.predict_proba(X)[0]
104
+ return float(probs[self.positive])
105
+
106
+ def save(self, model_dir: Path) -> None:
107
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
108
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
109
+ model_dir.mkdir(parents=True, exist_ok=True)
110
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
111
+ joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
112
+
113
+ @classmethod
114
+ def load(cls, model_dir: Path, **overrides) -> "Task5Method":
115
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
116
+ between here and the container -- the backbone path, the device."""
117
+ cfg = OmegaConf.merge(
118
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
119
+ )
120
+ method = cls(cfg)
121
+ state = joblib.load(model_dir / "head.joblib")
122
+ method.head, method.positive = state["head"], state["positive"]
123
+ return method
124
+
125
+
126
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
127
+
128
+ # Every image the task ships. The method picks which of them it wants, as at inference, where the
129
+ # challenge hands over the modalities whether or not a model uses them.
130
+ IMAGE_COLS = ("t1w",)
131
+
132
+
133
+ def cross_validate(
134
+ rows: list[dict], method: Task5Method, seed: int = 0, n_folds: int = 20
135
+ ) -> tuple[np.ndarray, np.ndarray]:
136
+ """Out-of-fold score for every subject, each predicted by a head fit on the other folds."""
137
+ y = np.array([row["label"] for row in rows])
138
+ oof = np.zeros(len(rows), dtype=float)
139
+ folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
140
+ start = time.perf_counter()
141
+ for fold, (train, test) in enumerate(folds.split(rows)):
142
+ method.fit([rows[i] for i in train])
143
+ for i in test:
144
+ oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
145
+ logger.info(
146
+ f"fold {fold + 1}/{n_folds} n={len(test)} y={y[test]} "
147
+ f"p={np.round(oof[test], 3)} ({time.perf_counter() - start:.0f}s)"
148
+ )
149
+ return y, oof
150
+
151
+
152
+ def score(
153
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
154
+ ) -> dict:
155
+ """AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
156
+ rng = np.random.default_rng(seed)
157
+ samples = []
158
+ for _ in range(n_boot):
159
+ rows = rng.integers(0, len(y), size=len(y))
160
+ if len(np.unique(y[rows])) < 2:
161
+ continue
162
+ samples.append(roc_auc_score(y[rows], oof[rows]))
163
+
164
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
165
+ return {
166
+ "auroc": float(roc_auc_score(y, oof)),
167
+ "auroc_ci_low": float(low),
168
+ "auroc_ci_high": float(high),
169
+ }
170
+
171
+
172
+ # ---- entrypoints ------------------------------------------------------------------------
173
+
174
+
175
+ def train(args: argparse.Namespace) -> None:
176
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
177
+ from fomo_tune.datasets import load_fomo_task5
178
+
179
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
180
+ run_dir = Path(cfg.output_root) / cfg.name
181
+ run_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ setup_logging(run_dir)
184
+ set_seed(cfg.seed)
185
+ logger.info(f"run {cfg.name} (git {git_sha()})")
186
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
187
+ OmegaConf.save(cfg, run_dir / "config.yaml")
188
+
189
+ rows = list(load_fomo_task5())
190
+ logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
191
+
192
+ method = Task5Method(cfg)
193
+ start = time.perf_counter()
194
+ y, oof = cross_validate(rows, method)
195
+ run_time = time.perf_counter() - start
196
+ summary = score(y, oof)
197
+
198
+ # the shipped head sees all n subjects, so it is not any of the models scored above
199
+ method.fit(rows)
200
+ method.save(run_dir / "model")
201
+
202
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
203
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
204
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
205
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
206
+
207
+
208
+ def predict(args: argparse.Namespace) -> None:
209
+ """The challenge contract: a t1 path in, one probability written to `--output`.
210
+
211
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
212
+ code cross-validation already ran, not something generated at build time.
213
+ """
214
+ overrides = {"device": args.device}
215
+ if args.ckpt_path:
216
+ overrides["ckpt_path"] = args.ckpt_path
217
+ method = Task5Method.load(args.model_dir, **overrides)
218
+
219
+ probability = method.predict({"t1w": nib.load(args.t1)})
220
+
221
+ args.output.write_text(f"{probability:.6f}\n")
222
+
223
+
224
+ def main() -> None:
225
+ parser = argparse.ArgumentParser()
226
+ modes = parser.add_subparsers(required=True)
227
+
228
+ train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
229
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
230
+ train_parser.set_defaults(run=train)
231
+
232
+ predict_parser = modes.add_parser("predict", help="one subject, one probability")
233
+ predict_parser.add_argument("--t1", type=Path, required=True)
234
+ predict_parser.add_argument("--output", type=Path, required=True)
235
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
236
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
237
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
238
+ predict_parser.set_defaults(run=predict)
239
+
240
+ args = parser.parse_args()
241
+ args.run(args)
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/utils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import random
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+ logger = logging.getLogger("fomo_tune")
11
+
12
+
13
+ def set_seed(seed: int) -> None:
14
+ random.seed(seed)
15
+ np.random.seed(seed)
16
+ torch.manual_seed(seed)
17
+
18
+
19
+ def git_sha() -> str:
20
+ kwargs = dict(cwd=Path(__file__).parent, capture_output=True, text=True, check=True)
21
+ sha = subprocess.run(["git", "rev-parse", "--short", "HEAD"], **kwargs).stdout.strip()
22
+ dirty = subprocess.run(["git", "status", "--porcelain", "-uno"], **kwargs).stdout.strip()
23
+ return f"{sha}-dirty" if dirty else sha
24
+
25
+
26
+ def setup_logging(run_dir: Path) -> None:
27
+ handlers = [logging.StreamHandler(sys.stdout), logging.FileHandler(run_dir / "log.txt")]
28
+ logger.setLevel(logging.INFO)
29
+ logger.handlers.clear()
30
+ for handler in handlers:
31
+ handler.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
32
+ logger.addHandler(handler)
33
+ logger.propagate = False
finetune/fomo_tune_baseline/output/task3/build/model/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task3
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task3
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/output/task3/build/predict.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+
4
+ from fomo_tune.main_task3 import main
5
+
6
+ sys.argv = [
7
+ sys.argv[0],
8
+ "predict",
9
+ *sys.argv[1:],
10
+ "--model-dir",
11
+ "/app/model",
12
+ "--ckpt-path",
13
+ "/app/model/backbone.pth",
14
+ ]
15
+
16
+ main()
finetune/fomo_tune_baseline/output/task3/build/smri_mae/main_pretrain.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Sophont, Inc
2
+ # This source code is licensed under the Apache License, Version 2.0
3
+ #
4
+ # References:
5
+ # deit: https://github.com/facebookresearch/deit/blob/main/main.py
6
+ # capi: https://github.com/facebookresearch/capi/blob/main/train_capi.py
7
+
8
+ import argparse
9
+ import datetime
10
+ import json
11
+ import math
12
+ import random
13
+ import subprocess
14
+ import time
15
+ from contextlib import nullcontext
16
+ from functools import partial
17
+ from itertools import islice
18
+ from pathlib import Path
19
+ from typing import Iterable, Sequence
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ import wandb
24
+ import webdataset as wds
25
+ from omegaconf import DictConfig, OmegaConf
26
+ from PIL import Image
27
+
28
+ from matplotlib import pyplot as plt
29
+ from torch import Tensor
30
+
31
+ import data.mri_data as mri_data
32
+ import smri_mae.model_mae as models_mae
33
+ import smri_mae.utils as ut
34
+ import smri_mae.visualization as vis
35
+
36
+ DEFAULT_CONFIG = Path(__file__).parent / "config/default_pretrain.yaml"
37
+
38
+ MODELS_DICT = models_mae.__dict__
39
+
40
+
41
+ def main(args: DictConfig):
42
+ # setup
43
+ ut.init_distributed_mode(args)
44
+ global_rank = ut.get_rank()
45
+ is_master = global_rank == 0
46
+ world_size = ut.get_world_size()
47
+ device = torch.device(args.device)
48
+ ut.configure_flash_sdpa()
49
+ ut.random_seed(args.seed, rank=global_rank)
50
+
51
+ if args.name and not args.output_dir.endswith(args.name):
52
+ args.output_dir = f"{args.output_dir}/{args.name}"
53
+ output_dir = Path(args.output_dir)
54
+
55
+ if is_master:
56
+ output_dir.mkdir(parents=True, exist_ok=True)
57
+ out_cfg_path = output_dir / "config.yaml"
58
+ if out_cfg_path.exists():
59
+ prev_cfg = OmegaConf.load(out_cfg_path)
60
+ assert args == prev_cfg, "current config doesn't match previous config"
61
+ else:
62
+ OmegaConf.save(args, out_cfg_path)
63
+
64
+ if args.wandb:
65
+ wandb.init(
66
+ entity=args.wandb_entity,
67
+ project=args.wandb_project,
68
+ name=args.name,
69
+ notes=args.notes,
70
+ config=OmegaConf.to_container(args),
71
+ )
72
+
73
+ ut.setup_for_distributed(log_path=output_dir / "log.txt")
74
+
75
+ print("pretraining 3D ViTMAE")
76
+ print(f"start: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
77
+ print(f"cwd: {Path.cwd()}")
78
+ print(ut.get_sha())
79
+ print("config:", OmegaConf.to_yaml(args), sep="\n")
80
+
81
+ # data loaders
82
+ train_loader, eval_loaders = create_data_loaders(args)
83
+
84
+ # model
85
+ model = MODELS_DICT[args.model](
86
+ img_size=args.img_size,
87
+ in_chans=args.get("in_chans", 1),
88
+ patch_size=args.patch_size,
89
+ **(args.get("model_kwargs") or {}),
90
+ )
91
+ model.to(device)
92
+ print("model:", model, sep="\n")
93
+ num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
94
+ print(f"num params: {num_params / 1e6:.1f}M")
95
+
96
+ model_without_ddp = model
97
+ if args.distributed:
98
+ model = torch.nn.parallel.DistributedDataParallel(
99
+ model,
100
+ device_ids=[args.gpu],
101
+ gradient_as_bucket_view=True,
102
+ )
103
+ model_without_ddp = model.module
104
+
105
+ # optimizer
106
+ total_batch_size = args.batch_size * args.accum_iter * world_size
107
+ print(
108
+ f"total batch size: {total_batch_size} = "
109
+ f"{args.batch_size} bs per gpu x {args.accum_iter} accum x {world_size} gpus"
110
+ )
111
+
112
+ if not args.get("lr"):
113
+ args.lr = args.base_lr * total_batch_size / 256
114
+ print(f"lr: {args.lr:.2e} = {args.base_lr:.2e} x {total_batch_size} / 256")
115
+ else:
116
+ print(f"lr: {args.lr:.2e}")
117
+
118
+ param_groups = ut.get_param_groups(model)
119
+ ut.update_lr(param_groups, args.lr)
120
+ ut.update_wd(param_groups, args.weight_decay)
121
+ # cast or else it corrupts the checkpoint
122
+ betas = tuple(args.betas) if args.betas is not None else None
123
+ optimizer = torch.optim.AdamW(param_groups, betas=betas, fused=True)
124
+
125
+ epoch_num_batches = len(train_loader)
126
+ steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
127
+ total_steps = args.epochs * steps_per_epoch
128
+ warmup_steps = args.warmup_epochs * steps_per_epoch
129
+ lr_schedule = ut.WarmupThenCosine(
130
+ base_value=args.lr,
131
+ final_value=args.min_lr,
132
+ total_iters=total_steps,
133
+ warmup_iters=warmup_steps,
134
+ )
135
+ print(f"full schedule: epochs = {args.epochs} (steps = {total_steps})")
136
+ print(f"warmup: epochs = {args.warmup_epochs} (steps = {warmup_steps})")
137
+
138
+ # loss scaling not needed for bfloat16 (according to timm)
139
+ if args.amp and args.amp_dtype != "bfloat16":
140
+ loss_scaler = torch.GradScaler(device.type)
141
+ else:
142
+ loss_scaler = None
143
+
144
+ # load checkpoint/resume training
145
+ ut.load_model(args, model_without_ddp, optimizer, loss_scaler)
146
+
147
+ print(f"start training for {args.epochs} epochs")
148
+ start_time = time.monotonic()
149
+ for epoch in range(args.start_epoch, args.epochs):
150
+ train_stats = train_one_epoch(
151
+ args,
152
+ model,
153
+ train_loader,
154
+ optimizer,
155
+ loss_scaler,
156
+ lr_schedule,
157
+ epoch,
158
+ device,
159
+ )
160
+ eval_stats = {}
161
+ eval_plots = {}
162
+ eval_period = args.get("eval_period", 1)
163
+ if eval_period and (epoch % eval_period == 0 or epoch == args.epochs - 1):
164
+ for name, loader in eval_loaders.items():
165
+ stats, plots = evaluate(
166
+ args,
167
+ model,
168
+ loader,
169
+ epoch,
170
+ device,
171
+ eval_name=name,
172
+ )
173
+ eval_stats.update(stats)
174
+ eval_plots.update(plots)
175
+
176
+ merged_stats = {"epoch": epoch, **train_stats, **eval_stats}
177
+ if is_master:
178
+ with (output_dir / "log.json").open("a") as f:
179
+ print(json.dumps(merged_stats), file=f)
180
+
181
+ for plot_name, img in eval_plots.items():
182
+ plot_name = plot_name.replace("/", "__")
183
+ img.save(output_dir / f"{plot_name}__{epoch:05d}.png")
184
+
185
+ ut.save_model(args, epoch, model_without_ddp, optimizer, loss_scaler)
186
+ sync_checkpoints_to_r2(args, output_dir)
187
+
188
+ if args.distributed:
189
+ torch.distributed.destroy_process_group()
190
+
191
+ total_time = time.monotonic() - start_time
192
+ print(f"done! training time: {datetime.timedelta(seconds=int(total_time))}")
193
+
194
+
195
+ def create_data_loaders(args: DictConfig):
196
+ data_loaders = {}
197
+ dataset_names = [args.train_dataset] + args.eval_datasets
198
+
199
+ for dataset_name in dataset_names:
200
+ dataset_config = args.datasets[dataset_name].copy()
201
+ drop_last = dataset_config.pop("drop_last")
202
+ is_train = dataset_name == args.train_dataset
203
+
204
+ print(f"loading dataset: {dataset_name}\n\n{OmegaConf.to_yaml(dataset_config)}")
205
+ shuffle = dataset_config["shuffle"]
206
+ samples_per_epoch = dataset_config.pop("samples_per_epoch")
207
+ dataset = mri_data.make_sparse_wds_dataset(
208
+ dataset_config["url"],
209
+ shuffle=shuffle,
210
+ buffer_size=dataset_config["buffer_size"],
211
+ )
212
+ num_workers = int(args.num_workers)
213
+ loader_kwargs = {
214
+ "batch_size": args.batch_size,
215
+ "collate_fn": partial(mri_data.collate, include_meta=not is_train),
216
+ "shuffle": False,
217
+ "num_workers": num_workers,
218
+ "persistent_workers": num_workers > 0,
219
+ "pin_memory": True,
220
+ "drop_last": drop_last,
221
+ "prefetch_factor": args.prefetch_factor,
222
+ }
223
+ loader = wds.WebLoader(dataset, **loader_kwargs)
224
+ num_batches = samples_per_epoch // (ut.get_world_size() * args.batch_size)
225
+ loader = loader.with_epoch(num_batches)
226
+ loader = loader.with_length(num_batches, silent=True)
227
+
228
+ data_loaders[dataset_name] = loader
229
+
230
+ train_loader = data_loaders.pop(args.train_dataset)
231
+ return train_loader, data_loaders
232
+
233
+
234
+ def sync_checkpoints_to_r2(args: DictConfig, output_dir: Path) -> None:
235
+ r2_sync_url = args.get("r2_sync")
236
+ if not r2_sync_url or not ut.is_main_process():
237
+ return
238
+
239
+ cmd = ["aws", "s3", "sync", str(output_dir), str(r2_sync_url), "--profile", "r2"]
240
+ print(f"syncing checkpoints to R2: {output_dir} -> {r2_sync_url}")
241
+ subprocess.run(cmd, check=True)
242
+
243
+
244
+ def train_one_epoch(
245
+ args: DictConfig,
246
+ model: nn.Module,
247
+ data_loader: Iterable,
248
+ optimizer: torch.optim.Optimizer,
249
+ loss_scaler: torch.GradScaler | None,
250
+ lr_schedule: Sequence[float],
251
+ epoch: int,
252
+ device: torch.device,
253
+ ):
254
+ model.train()
255
+
256
+ metric_logger = ut.MetricLogger(delimiter=" ")
257
+ metric_logger.add_meter("lr", ut.SmoothedValue(window_size=1, fmt="{value:.6f}"))
258
+ metric_logger.add_meter("grad", ut.SmoothedValue())
259
+ header = f"Train: [{epoch}]"
260
+ log_wandb = args.wandb and ut.is_main_process()
261
+
262
+ epoch_num_batches = len(data_loader)
263
+ steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
264
+
265
+ print_freq = args.get("print_freq", 100) if not args.debug else 1
266
+ num_batches = epoch_num_batches if not args.debug else 10
267
+ amp_dtype = getattr(torch, args.amp_dtype)
268
+ use_cuda = device.type == "cuda"
269
+ if use_cuda and args.presend_cuda:
270
+ data_loader = ut.pre_send_to_cuda_wrapper(
271
+ data_loader, device, dtype_map={torch.float16: amp_dtype}
272
+ )
273
+
274
+ optimizer.zero_grad()
275
+
276
+ for batch_idx, batch in enumerate(
277
+ metric_logger.log_every(data_loader, print_freq, header, total_steps=num_batches)
278
+ ):
279
+ if use_cuda and not args.presend_cuda:
280
+ batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
281
+
282
+ batch_step = batch_idx + 1
283
+ log_step = batch_step % print_freq == 0 or batch_step == num_batches
284
+ update_in_epoch = batch_idx // args.accum_iter
285
+ group_size = min(args.accum_iter, num_batches - update_in_epoch * args.accum_iter)
286
+ need_update = batch_step % args.accum_iter == 0 or batch_step == num_batches
287
+ global_step = epoch * steps_per_epoch + update_in_epoch
288
+ lr = lr_schedule[global_step]
289
+ if need_update:
290
+ ut.update_lr(optimizer.param_groups, lr)
291
+
292
+ images, img_mask = mri_data.densify_sparse_image_batch(
293
+ batch["image_values"],
294
+ batch["img_mask"],
295
+ (int(args.get("in_chans", 1)), *args.img_size),
296
+ dtype=amp_dtype,
297
+ )
298
+
299
+ sync_context = model.no_sync() if args.distributed and not need_update else nullcontext()
300
+ with sync_context:
301
+ with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
302
+ loss = model(
303
+ images,
304
+ img_mask=img_mask,
305
+ mask_ratio=args.mask_ratio,
306
+ pred_mask_ratio=args.pred_mask_ratio,
307
+ pad_to_multiple=args.pad_to_multiple,
308
+ with_state=False,
309
+ )
310
+
311
+ loss_for_log = loss.detach()
312
+ torch._assert_async(torch.isfinite(loss_for_log), "non-finite loss")
313
+
314
+ grad_norm = ut.backward_step(
315
+ loss / group_size,
316
+ optimizer,
317
+ scaler=loss_scaler,
318
+ need_update=need_update,
319
+ max_norm=args.clip_grad,
320
+ )
321
+
322
+ if need_update and log_step:
323
+ loss_value = loss_for_log.item()
324
+ grad_norm_value = grad_norm.item()
325
+ metric_logger.update(loss=loss_value, lr=lr, grad=grad_norm_value)
326
+ if log_wandb:
327
+ wandb.log(
328
+ {
329
+ "train/loss": loss_value,
330
+ "train/lr": lr,
331
+ "train/grad": grad_norm_value,
332
+ },
333
+ step=int(1000 * (epoch + batch_step / epoch_num_batches)),
334
+ )
335
+
336
+ # gather the stats from all processes
337
+ metric_logger.synchronize_between_processes()
338
+ print("Averaged stats:", metric_logger)
339
+ return {f"train/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
340
+
341
+
342
+ @torch.inference_mode()
343
+ def evaluate(
344
+ args: DictConfig,
345
+ model: nn.Module,
346
+ data_loader: Iterable,
347
+ epoch: int,
348
+ device: torch.device,
349
+ eval_name: str,
350
+ ):
351
+ model.eval()
352
+
353
+ metric_logger = ut.MetricLogger(delimiter=" ")
354
+ header = f"Eval ({eval_name}): [{epoch}]"
355
+ is_master = ut.is_main_process()
356
+ log_wandb = args.wandb and is_master
357
+
358
+ epoch_num_batches = len(data_loader)
359
+ if epoch_num_batches <= 0:
360
+ raise ValueError(f"eval loader {eval_name!r} has zero batches")
361
+
362
+ print_freq = args.get("print_freq", 100) if not args.debug else 1
363
+ num_batches = epoch_num_batches if not args.debug else 10
364
+ num_batches = min(num_batches, epoch_num_batches)
365
+ eval_seed = int(args.get("eval_seed", args.seed)) + ut.get_rank()
366
+ example_step = random.Random(eval_seed).randint(1, num_batches)
367
+ amp_dtype = getattr(torch, args.amp_dtype)
368
+ use_cuda = device.type == "cuda"
369
+ rng_state = ut.capture_rng_state()
370
+ torch.set_rng_state(torch.Generator().manual_seed(eval_seed).get_state())
371
+ if use_cuda:
372
+ torch.cuda.manual_seed(eval_seed)
373
+ if use_cuda and args.presend_cuda:
374
+ data_loader = ut.pre_send_to_cuda_wrapper(
375
+ data_loader, device, dtype_map={torch.float16: amp_dtype}
376
+ )
377
+
378
+ eval_batches = islice(data_loader, num_batches)
379
+ for batch_idx, batch in enumerate(
380
+ metric_logger.log_every(eval_batches, print_freq, header, total_steps=num_batches)
381
+ ):
382
+ if use_cuda and not args.presend_cuda:
383
+ batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
384
+
385
+ batch_step = batch_idx + 1
386
+
387
+ images, img_mask = mri_data.densify_sparse_image_batch(
388
+ batch["image_values"],
389
+ batch["img_mask"],
390
+ (int(args.get("in_chans", 1)), *args.img_size),
391
+ dtype=amp_dtype,
392
+ )
393
+
394
+ with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
395
+ loss, state = model(
396
+ images,
397
+ img_mask=img_mask,
398
+ mask_ratio=args.mask_ratio,
399
+ pred_mask_ratio=args.pred_mask_ratio,
400
+ pad_to_multiple=args.pad_to_multiple,
401
+ )
402
+
403
+ loss_value = loss.detach().float().item()
404
+ finite = torch.tensor(int(math.isfinite(loss_value)), dtype=torch.int32, device=device)
405
+ if args.distributed:
406
+ torch.distributed.all_reduce(finite, op=torch.distributed.ReduceOp.MIN)
407
+ if not finite.item():
408
+ raise RuntimeError("non-finite validation loss detected")
409
+ metric_logger.meters["loss"].update(loss_value, n=int(batch["img_mask"].shape[0]))
410
+
411
+ if is_master and batch_step == example_step:
412
+ example_batch = {"image": images[:1], "img_mask": img_mask[:1]}
413
+ if "meta" in batch:
414
+ example_batch["meta"] = batch["meta"][:1]
415
+ example_state = {
416
+ "pred_images": state["pred_images"][:1],
417
+ "pred_mask": state["pred_mask"][:1],
418
+ }
419
+ example_data = {
420
+ "batch": ut.send_data(example_batch, "cpu"),
421
+ "state": ut.send_data(example_state, "cpu"),
422
+ }
423
+
424
+ # gather the stats from all processes
425
+ metric_logger.synchronize_between_processes()
426
+ print(f"Averaged stats ({eval_name}):", metric_logger)
427
+ stats = {f"eval/{eval_name}/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
428
+
429
+ plots = {}
430
+ if is_master:
431
+ print(f"Making plots ({eval_name}): example={example_step}")
432
+ plots = make_plots(args, **example_data)
433
+ plots = {f"eval/{eval_name}/{k}": img for k, img in plots.items()}
434
+
435
+ if log_wandb:
436
+ wandb.log(stats, step=1000 * (epoch + 1))
437
+ wandb.log(
438
+ {k: wandb.Image(img, caption=f"example={example_step}") for k, img in plots.items()},
439
+ step=1000 * (epoch + 1),
440
+ )
441
+ ut.restore_rng_state(rng_state)
442
+ return stats, plots
443
+
444
+
445
+ def make_plots(
446
+ args: DictConfig,
447
+ batch: dict[str, Tensor],
448
+ state: dict[str, Tensor],
449
+ ) -> dict[str, Image.Image]:
450
+ fig_kwargs = args.get("fig_kwargs", {})
451
+
452
+ images = batch["image"]
453
+ img_mask = batch.get("img_mask")
454
+ if img_mask is not None:
455
+ img_mask = img_mask.expand_as(images)
456
+
457
+ raw_mean, raw_std = vis.raw_stats_from_batch(batch)
458
+
459
+ plots = {}
460
+ mask_pred_fig = vis.plot_mask_pred(
461
+ target=images,
462
+ pred=state["pred_images"],
463
+ pred_mask=state["pred_mask"],
464
+ img_mask=img_mask,
465
+ patch_size=args.patch_size,
466
+ raw_mean=raw_mean,
467
+ raw_std=raw_std,
468
+ **ut.filter_kwargs(vis.plot_mask_pred, fig_kwargs),
469
+ )
470
+ plots["mask_pred"] = vis.fig2pil(mask_pred_fig)
471
+ plt.close(mask_pred_fig)
472
+
473
+ return plots
474
+
475
+
476
+ if __name__ == "__main__":
477
+ parser = argparse.ArgumentParser()
478
+ parser.add_argument("--cfg-path", type=str, default=None)
479
+ parser.add_argument("--overrides", type=str, default=None, nargs="+")
480
+ args = parser.parse_args()
481
+ cfg = OmegaConf.load(DEFAULT_CONFIG)
482
+ if args.cfg_path:
483
+ cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.load(args.cfg_path))
484
+ if args.overrides:
485
+ cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.from_dotlist(args.overrides))
486
+ main(cfg)
finetune/fomo_tune_baseline/output/task3/build/smri_mae/model_mae.py ADDED
@@ -0,0 +1,916 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Sophont, Inc
2
+ # This source code is licensed under the Apache License, Version 2.0
3
+ #
4
+ # References:
5
+ # capi: https://github.com/facebookresearch/capi/blob/main/model.py
6
+ # timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
7
+
8
+ """
9
+ From-scratch re-implementation of the original MAE model.
10
+
11
+ MaskedEncoder: standard ViT with masking
12
+ MaskedDecoder: standard self-attention MAE decoder
13
+ MaskedAutoEncoderViT: full MAE model for 3D structural MRI volumes
14
+ """
15
+
16
+ from collections.abc import Sequence
17
+ from typing import Literal
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from torch import Tensor
23
+ from torch.utils.checkpoint import checkpoint
24
+ from huggingface_hub import PyTorchModelHubMixin
25
+ from jaxtyping import Float, Int
26
+
27
+ from .modules import (
28
+ AbsolutePosEmbed,
29
+ Block,
30
+ LayerNorm,
31
+ Normalize,
32
+ JaggedBatch,
33
+ Patchify3D,
34
+ SeparablePosEmbed,
35
+ SinCosPosEmbed3D,
36
+ unpack_tokens,
37
+ )
38
+ from .masking import pad_patch_mask
39
+
40
+
41
+ class MaskedEncoder(nn.Module):
42
+ """
43
+ Masked transformer encoder.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ patchify: nn.Module,
49
+ patch_embed: nn.Module,
50
+ pos_embed: nn.Module,
51
+ depth: int = 12,
52
+ embed_dim: int = 768,
53
+ num_heads: int = 12,
54
+ qkv_bias: bool = True,
55
+ proj_bias: bool = True,
56
+ mlp_ratio: int | float = 4,
57
+ class_token: bool = True,
58
+ reg_tokens: int = 0,
59
+ no_embed_class: bool = False,
60
+ final_norm: bool = True,
61
+ drop_path_rate: float = 0.0,
62
+ mask_drop_scale: bool = False,
63
+ ):
64
+ super().__init__()
65
+ self.num_prefix_tokens = int(class_token) + reg_tokens
66
+ self.num_reg_tokens = reg_tokens
67
+ self.has_class_token = class_token
68
+ self.no_embed_class = no_embed_class
69
+
70
+ # scale inputs by 1 / observed rate (like dropout)
71
+ self.mask_drop_scale = mask_drop_scale
72
+
73
+ # inject tokenization modules, so that the encoder doesn't specifically need to
74
+ # know how the data are tokenized, while still implementing a complete
75
+ # self-contained model.
76
+ self.patchify = patchify
77
+ self.patch_embed = patch_embed
78
+ self.pos_embed = pos_embed
79
+
80
+ R = reg_tokens
81
+ self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
82
+ self.reg_token = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
83
+
84
+ if not no_embed_class:
85
+ self.cls_token_pos = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
86
+ self.reg_token_pos = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
87
+ else:
88
+ self.cls_token_pos = self.reg_token_pos = None
89
+
90
+ # stochastic depth decay rule
91
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
92
+
93
+ self.blocks = nn.ModuleList(
94
+ [
95
+ Block(
96
+ dim=embed_dim,
97
+ num_heads=num_heads,
98
+ qkv_bias=qkv_bias,
99
+ proj_bias=proj_bias,
100
+ mlp_ratio=mlp_ratio,
101
+ drop_path=dpr[ii],
102
+ )
103
+ for ii in range(depth)
104
+ ]
105
+ )
106
+
107
+ self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
108
+
109
+ self.reset_parameters()
110
+
111
+ def extra_repr(self):
112
+ return (
113
+ f"class_token={self.has_class_token}, reg_tokens={self.num_reg_tokens}, "
114
+ f"no_embed_class={self.no_embed_class}, mask_drop_scale={self.mask_drop_scale}"
115
+ )
116
+
117
+ def reset_parameters(self) -> None:
118
+ for p in [self.cls_token, self.cls_token_pos, self.reg_token, self.reg_token_pos]:
119
+ if p is not None:
120
+ nn.init.trunc_normal_(p, std=0.02)
121
+
122
+ def cat_tokens(self, x: Tensor) -> Tensor:
123
+ # prepend cls and reg tokens with optional learned position embedding
124
+ # the cls and reg pos embedding is ofc redundant, but included in many other
125
+ # implementations.
126
+ B, _, _ = x.shape
127
+
128
+ to_cat = []
129
+ if self.has_class_token:
130
+ cls_token = self.cls_token
131
+ if not self.no_embed_class:
132
+ cls_token = cls_token + self.cls_token_pos
133
+ to_cat.append(cls_token.expand(B, -1, -1))
134
+
135
+ if self.num_reg_tokens:
136
+ reg_token = self.reg_token
137
+ if not self.no_embed_class:
138
+ reg_token = reg_token + self.reg_token_pos
139
+ to_cat.append(reg_token.expand(B, -1, -1))
140
+
141
+ if to_cat:
142
+ x = torch.cat(to_cat + [x], dim=1)
143
+ return x
144
+
145
+ def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
146
+ if self.num_prefix_tokens:
147
+ prefix_mask = torch.ones(
148
+ (batch_size, self.num_prefix_tokens),
149
+ dtype=torch.bool,
150
+ device=token_mask.device,
151
+ )
152
+ token_mask = torch.cat([prefix_mask, token_mask], dim=1)
153
+ return token_mask
154
+
155
+ def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor | None, Tensor]:
156
+ cls_offset = int(self.has_class_token)
157
+ cls = x[:, :cls_offset] if self.has_class_token else None
158
+ if self.num_reg_tokens:
159
+ reg = x[:, cls_offset : self.num_prefix_tokens, :]
160
+ else:
161
+ reg = None
162
+ patch = x[:, self.num_prefix_tokens :, :]
163
+ return cls, reg, patch
164
+
165
+ def forward(
166
+ self,
167
+ x: Tensor,
168
+ mask: Tensor | None = None,
169
+ mask_ratio: float | None = None,
170
+ pad_to_multiple: int | None = None,
171
+ ) -> tuple[
172
+ Float[Tensor, "B 1 D"] | None,
173
+ Float[Tensor, "B R D"] | None,
174
+ Float[Tensor, "B L D"],
175
+ Tensor | None,
176
+ Int[Tensor, "B L"] | None,
177
+ Tensor | None,
178
+ ]:
179
+ """
180
+ x: input data shape [B, C, D, H, W]
181
+ mask: visible mask, 1 = visible, 0 = invisible. broadcastable shape
182
+ mask_ratio: mask ratio for uniform random masking
183
+
184
+ returns:
185
+ - cls_embeds: [B, 1, D]
186
+ - reg_embeds: [B, R, D]
187
+ - patch_embeds: [B, L, D], where L is the number of visible patches
188
+ - mask: observed mask, 1 = observed, 0 = unobserved. same shape as input
189
+ - mask_ids: indices of visible patches [B L]
190
+ - token_mask: valid token mask for padded per-sample masking [B L]
191
+ """
192
+ # apply mask to the input
193
+ if mask is not None:
194
+ mask = mask.to(device=x.device, dtype=torch.bool).expand_as(x)
195
+ x = x.masked_fill(~mask, 0)
196
+
197
+ # patchify input
198
+ x = self.patchify(x)
199
+ B, N, P = x.shape
200
+
201
+ # patchify mask and apply dropout style scaling
202
+ if mask is not None:
203
+ mask_patches = self.patchify(mask)
204
+ patch_num_obs = mask_patches.sum(dim=-1)
205
+ patch_mask = patch_num_obs > 0
206
+ if self.mask_drop_scale:
207
+ patch_num_obs = patch_num_obs.to(x.dtype)
208
+ x = x * (P / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
209
+ elif mask_ratio is not None:
210
+ patch_mask = torch.ones((B, N), dtype=torch.bool, device=x.device)
211
+ mask_patches = patch_mask.unsqueeze(-1).expand(-1, -1, P)
212
+ else:
213
+ patch_mask = mask_patches = None
214
+
215
+ # patch and position embed
216
+ x = self.patch_embed(x)
217
+ x = self.pos_embed(x)
218
+
219
+ if mask is not None or mask_ratio is not None:
220
+ mask_ratio = 0.0 if mask_ratio is None else mask_ratio
221
+ patch_mask, mask_ids, token_mask = pad_patch_mask(
222
+ patch_mask,
223
+ mask_ratio=mask_ratio,
224
+ shuffle=mask_ratio > 0,
225
+ pad_to_multiple=pad_to_multiple,
226
+ )
227
+
228
+ mask_patches = mask_patches & patch_mask.unsqueeze(-1)
229
+ mask = self.patchify.unpatchify(mask_patches)
230
+ x = x.gather(1, mask_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
231
+ else:
232
+ mask_ids = None
233
+ token_mask = None
234
+
235
+ cls_embeds, reg_embeds, patch_embeds = self.forward_patch_embeds(
236
+ x,
237
+ token_mask=token_mask,
238
+ )
239
+ return cls_embeds, reg_embeds, patch_embeds, mask, mask_ids, token_mask
240
+
241
+ def forward_patch_embeds(
242
+ self,
243
+ x: Float[Tensor, "B L D"],
244
+ token_mask: Tensor | None = None,
245
+ ) -> tuple[
246
+ Float[Tensor, "B 1 D"] | None,
247
+ Float[Tensor, "B R D"] | None,
248
+ Float[Tensor, "B L D"],
249
+ ]:
250
+ B = x.shape[0]
251
+ if token_mask is None:
252
+ token_mask = torch.ones(x.shape[:2], dtype=torch.bool, device=x.device)
253
+ x = self.cat_tokens(x)
254
+ token_mask = self.cat_token_mask(token_mask, B)
255
+ jagged_batch = JaggedBatch.from_mask(token_mask)
256
+ x = x[token_mask]
257
+ for block in self.blocks:
258
+ x = block(x, jagged_batch=jagged_batch)
259
+ x = self.norm(x)
260
+ x = unpack_tokens(x, token_mask)
261
+
262
+ cls_embeds, reg_embeds, patch_embeds = self.chunk_tokens(x)
263
+ return cls_embeds, reg_embeds, patch_embeds
264
+
265
+ def forward_visible_ids(
266
+ self,
267
+ x: Tensor,
268
+ visible_ids: Int[Tensor, "B L"],
269
+ img_mask: Tensor | None = None,
270
+ ) -> tuple[
271
+ Float[Tensor, "B 1 D"] | None,
272
+ Float[Tensor, "B R D"] | None,
273
+ Float[Tensor, "B L D"],
274
+ ]:
275
+ if img_mask is not None:
276
+ img_mask = img_mask.to(device=x.device, dtype=torch.bool).expand_as(x)
277
+ x = x.masked_fill(~img_mask, 0)
278
+
279
+ x = self.patchify(x)
280
+ if self.mask_drop_scale and img_mask is not None:
281
+ mask_patches = self.patchify(img_mask)
282
+ patch_num_obs = mask_patches.sum(dim=-1).to(x.dtype)
283
+ x = x * (self.patchify.patch_dim / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
284
+ x = self.patch_embed(x)
285
+ x = self.pos_embed(x)
286
+ visible_ids = visible_ids.to(device=x.device)
287
+ x = x.gather(1, visible_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
288
+ return self.forward_patch_embeds(x)
289
+
290
+ def forward_embedding(
291
+ self,
292
+ x: Tensor,
293
+ mask: Tensor | None = None,
294
+ mask_ratio: float | None = None,
295
+ ):
296
+ cls_embeds, reg_embeds, patch_embeds, *_ = self.forward(
297
+ x,
298
+ mask=mask,
299
+ mask_ratio=mask_ratio,
300
+ )
301
+ return cls_embeds, reg_embeds, patch_embeds
302
+
303
+
304
+ class MaskedDecoder(nn.Module):
305
+ """Self-attention MAE decoder supporting sparse subset decoding via pred_ids."""
306
+
307
+ def __init__(
308
+ self,
309
+ pos_embed: nn.Module,
310
+ head: nn.Module | None = None,
311
+ input_dim: int | None = None,
312
+ depth: int = 12,
313
+ embed_dim: int = 768,
314
+ num_heads: int = 12,
315
+ qkv_bias: bool = True,
316
+ proj_bias: bool = True,
317
+ mlp_ratio: int | float = 4,
318
+ class_token: bool = True,
319
+ no_embed_class: bool = False,
320
+ final_norm: bool = True,
321
+ ):
322
+ super().__init__()
323
+ input_dim = embed_dim if input_dim is None else input_dim
324
+ self.has_class_token = class_token
325
+ self.no_embed_class = no_embed_class
326
+
327
+ self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
328
+ self.cls_token_pos = (
329
+ nn.Parameter(torch.empty(1, 1, embed_dim))
330
+ if class_token and not no_embed_class
331
+ else None
332
+ )
333
+ self.mask_token = nn.Parameter(torch.empty(1, 1, embed_dim))
334
+
335
+ # decoder position embedding, encodes query position information into masks
336
+ self.pos_embed = pos_embed
337
+
338
+ self.proj = nn.Identity() if input_dim == embed_dim else nn.Linear(input_dim, embed_dim)
339
+
340
+ self.blocks = nn.ModuleList(
341
+ [
342
+ Block(
343
+ dim=embed_dim,
344
+ num_heads=num_heads,
345
+ qkv_bias=qkv_bias,
346
+ proj_bias=proj_bias,
347
+ mlp_ratio=mlp_ratio,
348
+ )
349
+ for _ in range(depth)
350
+ ]
351
+ )
352
+
353
+ self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
354
+
355
+ # optional injected prediction head
356
+ self.head = nn.Identity() if head is None else head
357
+
358
+ self.reset_parameters()
359
+
360
+ def extra_repr(self):
361
+ return f"class_token={self.has_class_token}, no_embed_class={self.no_embed_class}"
362
+
363
+ def reset_parameters(self) -> None:
364
+ # official mae initializes decoder cls token to zeros
365
+ # although perhaps this was an oversight
366
+ if self.cls_token is not None:
367
+ nn.init.zeros_(self.cls_token)
368
+ if self.cls_token_pos is not None:
369
+ nn.init.trunc_normal_(self.cls_token_pos, std=0.02)
370
+ nn.init.trunc_normal_(self.mask_token, std=0.02)
371
+
372
+ def cat_tokens(self, x: Tensor) -> Tensor:
373
+ if not self.has_class_token:
374
+ return x
375
+ cls_token = self.cls_token
376
+ if not self.no_embed_class:
377
+ cls_token = cls_token + self.cls_token_pos
378
+ return torch.cat([cls_token.expand(x.shape[0], -1, -1), x], dim=1)
379
+
380
+ def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
381
+ if self.has_class_token:
382
+ cls_mask = torch.ones(
383
+ (batch_size, 1),
384
+ dtype=torch.bool,
385
+ device=token_mask.device,
386
+ )
387
+ token_mask = torch.cat([cls_mask, token_mask], dim=1)
388
+ return token_mask
389
+
390
+ def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor]:
391
+ cls_offset = int(self.has_class_token)
392
+ cls = x[:, :cls_offset] if self.has_class_token else None
393
+ patch = x[:, cls_offset:, :]
394
+ return cls, patch
395
+
396
+ def forward(
397
+ self,
398
+ embeds: Float[Tensor, "B L D"],
399
+ embed_ids: Int[Tensor, "B L"] | None = None,
400
+ pred_ids: Int[Tensor, "B Q"] | None = None,
401
+ embed_token_mask: Tensor | None = None,
402
+ pred_token_mask: Tensor | None = None,
403
+ packed_output: bool = False,
404
+ ) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
405
+ """
406
+ embeds: input patch embeddings.
407
+ embed_ids: optional patch indices for input embeddings. If not provided, no
408
+ position will be added to the embeddings.
409
+ pred_ids: patch indices of query mask positions. If None, decode *all* patches.
410
+
411
+ returns:
412
+ - pred [B, Q, P] where Q is the number of prediction patches and P is the output
413
+ dimension
414
+ """
415
+ B, L, _ = embeds.shape
416
+
417
+ Q = self.pos_embed.num_patches if pred_ids is None else pred_ids.shape[1]
418
+ mask = self.mask_token.expand(B, Q, -1)
419
+ mask = self.pos_embed(mask, pos_ids=pred_ids)
420
+
421
+ embeds = self.proj(embeds)
422
+
423
+ if embed_ids is not None:
424
+ embeds = self.pos_embed(embeds, pos_ids=embed_ids)
425
+ if embed_token_mask is None:
426
+ embed_token_mask = torch.ones((B, L), dtype=torch.bool, device=embeds.device)
427
+ if pred_token_mask is None:
428
+ pred_token_mask = torch.ones((B, Q), dtype=torch.bool, device=embeds.device)
429
+ x = torch.cat([embeds, mask], dim=1)
430
+ token_mask = torch.cat([embed_token_mask, pred_token_mask], dim=1)
431
+
432
+ x = self.cat_tokens(x)
433
+ token_mask = self.cat_token_mask(token_mask, B)
434
+ jagged_batch = JaggedBatch.from_mask(token_mask)
435
+ x = x[token_mask]
436
+ # Keep headroom for rare maximum-length PSP batches.
437
+ checkpoint_start = max(0, len(self.blocks) - 2)
438
+ for block_index, block in enumerate(self.blocks):
439
+ if self.training and torch.is_grad_enabled() and block_index >= checkpoint_start:
440
+ x = checkpoint(block, x, jagged_batch, use_reentrant=False)
441
+ else:
442
+ x = block(x, jagged_batch=jagged_batch)
443
+
444
+ x = self.norm(x)
445
+ if packed_output:
446
+ pred_offset = int(self.has_class_token) + L
447
+ prediction_mask = F.pad(pred_token_mask, (pred_offset, 0))
448
+ return self.head(x[prediction_mask[token_mask]])
449
+
450
+ x = unpack_tokens(x, token_mask)
451
+ _, x = self.chunk_tokens(x)
452
+
453
+ pred = x[:, L:]
454
+ pred = pred.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
455
+ pred = self.head(pred)
456
+ return pred
457
+
458
+
459
+ class MaskedAutoencoderViT(nn.Module, PyTorchModelHubMixin):
460
+ def __init__(
461
+ self,
462
+ img_size: int | tuple[int, int, int] = (208, 240, 208),
463
+ patch_size: int | tuple[int, int, int] = (16, 16, 16),
464
+ in_chans: int = 1,
465
+ depth: int = 12,
466
+ embed_dim: int = 768,
467
+ num_heads: int = 12,
468
+ decoder_depth: int = 4,
469
+ decoder_embed_dim: int | None = 512,
470
+ decoder_num_heads: int | None = 16, # default from mae, head dim = 32
471
+ qkv_bias: bool = True,
472
+ proj_bias: bool = True,
473
+ mlp_ratio: int | float = 4,
474
+ class_token: bool = True,
475
+ reg_tokens: int = 0,
476
+ no_embed_class: bool = False,
477
+ drop_path_rate: float = 0.0,
478
+ mask_drop_scale: bool = False,
479
+ no_decode_pos: bool = False,
480
+ pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
481
+ target_norm: Literal["none", "global", "slice", "patch"] | None = None,
482
+ ):
483
+ super().__init__()
484
+ img_size = _to_3d_tuple(img_size, "img_size")
485
+ patch_size = _to_3d_tuple(patch_size, "patch_size")
486
+
487
+ self.no_decode_pos = no_decode_pos # don't pos encode embeddings in decoder
488
+
489
+ # patchify reshapes input into sequence of flattened patches, shape [B, N, P]
490
+ ndim = 3
491
+ patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
492
+
493
+ # linear patch embedding P -> D
494
+ patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
495
+
496
+ # position embedding
497
+ # separable position embedding decouples the first spatial axis from the
498
+ # others. Fixed sin/cos embeddings are the default for sMRI volumes.
499
+ if pos_embed == "sincos":
500
+ pos_embed_layer = SinCosPosEmbed3D
501
+ else:
502
+ pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
503
+ pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
504
+
505
+ # encoder. for inference, this model can be extracted and used like a regular vit
506
+ self.encoder = MaskedEncoder(
507
+ patchify=patchify,
508
+ patch_embed=patch_embed,
509
+ pos_embed=pos_embed,
510
+ depth=depth,
511
+ embed_dim=embed_dim,
512
+ num_heads=num_heads,
513
+ qkv_bias=qkv_bias,
514
+ proj_bias=proj_bias,
515
+ mlp_ratio=mlp_ratio,
516
+ class_token=class_token,
517
+ reg_tokens=reg_tokens,
518
+ no_embed_class=no_embed_class,
519
+ drop_path_rate=drop_path_rate,
520
+ mask_drop_scale=mask_drop_scale,
521
+ )
522
+
523
+ self.pred_patchify = patchify
524
+
525
+ # fall back to encoder architecture width
526
+ decoder_embed_dim = decoder_embed_dim or embed_dim
527
+ decoder_num_heads = decoder_num_heads or num_heads
528
+
529
+ decoder_pos_embed = pos_embed_layer(decoder_embed_dim, self.pred_patchify.grid_size)
530
+ # we might want to try tying the weights of the prediction head to the patch
531
+ # embedding at some point.
532
+ decoder_head = nn.Linear(decoder_embed_dim, self.pred_patchify.patch_dim)
533
+
534
+ self.decoder = MaskedDecoder(
535
+ pos_embed=decoder_pos_embed,
536
+ head=decoder_head,
537
+ input_dim=embed_dim,
538
+ depth=decoder_depth,
539
+ embed_dim=decoder_embed_dim,
540
+ num_heads=decoder_num_heads,
541
+ qkv_bias=qkv_bias,
542
+ proj_bias=proj_bias,
543
+ mlp_ratio=mlp_ratio,
544
+ class_token=class_token,
545
+ no_embed_class=no_embed_class,
546
+ )
547
+
548
+ # mae style target normalization
549
+ # dim is relative to an unflattened embedding tensor of shape [B, *grid_size, D]
550
+ if target_norm not in {"none", None}:
551
+ norm_dim = {
552
+ "global": tuple(range(1, ndim + 2)), # full sequence
553
+ "slice": tuple(range(2, ndim + 2)), # each depth slice along first dim
554
+ "patch": -1, # normalize each patch independently (mae pix norm loss)
555
+ }[target_norm]
556
+ self.target_norm = Normalize(self.pred_patchify.grid_size, dim=norm_dim)
557
+ else:
558
+ self.target_norm = None
559
+
560
+ self.init_weights()
561
+
562
+ def extra_repr(self):
563
+ return f"no_decode_pos={self.no_decode_pos}"
564
+
565
+ def init_weights(self):
566
+ self.apply(_init_weights)
567
+
568
+ def prepare_targets(self, images: Tensor, img_mask: Tensor | None):
569
+ """
570
+ images: [B, C, D, H, W]
571
+ img_mask: mask of valid data. only used for computing correct normalization
572
+ stats. same shape as images.
573
+ """
574
+ targets_patches = self.pred_patchify(images) # [B, N, P]
575
+
576
+ # target normalization
577
+ if self.target_norm is not None:
578
+ # full image data mask used for normalization stats only
579
+ if img_mask is not None:
580
+ img_mask_patches = self.pred_patchify(img_mask)
581
+ else:
582
+ img_mask_patches = None
583
+ targets_patches, *targets_stats = self.target_norm(targets_patches, img_mask_patches)
584
+ else:
585
+ targets_stats = None
586
+
587
+ return targets_patches, targets_stats
588
+
589
+ def prepare_masks(
590
+ self,
591
+ img_mask: Tensor,
592
+ visible_mask: Tensor | None,
593
+ pred_mask: Tensor | None,
594
+ device: torch.device,
595
+ ):
596
+ img_mask = img_mask.to(device=device, dtype=torch.bool)
597
+
598
+ if visible_mask is None:
599
+ visible_mask = img_mask
600
+ else:
601
+ visible_mask = img_mask & visible_mask.to(device=device, dtype=torch.bool)
602
+
603
+ if pred_mask is None:
604
+ pred_mask = img_mask
605
+ else:
606
+ pred_mask = img_mask & pred_mask.to(device=device, dtype=torch.bool)
607
+
608
+ return img_mask, visible_mask, pred_mask
609
+
610
+ def prepare_pred_mask(
611
+ self,
612
+ visible_mask: Tensor,
613
+ pred_mask: Tensor | None = None,
614
+ pred_mask_ratio: float | None = None,
615
+ pad_to_multiple: int | None = None,
616
+ ):
617
+ """
618
+ prepare prediction mask by removing visible content
619
+ visible_mask: [B, C, D, H, W], 1 = visible, 0 = invisible
620
+ pred_mask: same shape, 1 = predict, 0 = don't predict
621
+ """
622
+ if pred_mask is None:
623
+ pred_mask = torch.ones_like(visible_mask)
624
+
625
+ pred_mask = pred_mask & ~visible_mask
626
+
627
+ pred_mask_patches = self.pred_patchify(pred_mask)
628
+ pred_patch_mask = pred_mask_patches.any(dim=-1)
629
+ # Optionally subsample the prediction candidates.
630
+ mask_ratio = 0.0 if pred_mask_ratio is None else pred_mask_ratio
631
+ pred_patch_mask, pred_ids, pred_token_mask = pad_patch_mask(
632
+ pred_patch_mask,
633
+ mask_ratio=mask_ratio,
634
+ # With per-sample padding every candidate is retained when the ratio
635
+ # is zero, so randomizing their order is pure overhead.
636
+ shuffle=mask_ratio > 0,
637
+ pad_to_multiple=pad_to_multiple,
638
+ )
639
+ pred_mask_patches = pred_mask_patches & pred_patch_mask.unsqueeze(-1)
640
+ return pred_mask_patches, pred_ids, pred_token_mask
641
+
642
+ def forward_decoder(
643
+ self,
644
+ patch_embeds: Float[Tensor, "B L D"],
645
+ visible_ids: Int[Tensor, "B L"],
646
+ pred_ids: Int[Tensor, "B Q"] | None,
647
+ visible_token_mask: Tensor | None = None,
648
+ pred_token_mask: Tensor | None = None,
649
+ packed_output: bool = False,
650
+ ) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
651
+ return self.decoder(
652
+ patch_embeds,
653
+ embed_ids=None if self.no_decode_pos else visible_ids,
654
+ pred_ids=pred_ids,
655
+ embed_token_mask=visible_token_mask,
656
+ pred_token_mask=pred_token_mask,
657
+ packed_output=packed_output,
658
+ )
659
+
660
+ def forward_loss(
661
+ self,
662
+ preds: Float[Tensor, "T P"],
663
+ targets_patches: Float[Tensor, "B N P"],
664
+ pred_mask_patches: Float[Tensor, "B N P"],
665
+ pred_ids: Int[Tensor, "B Q"],
666
+ pred_token_mask: Tensor,
667
+ ) -> Tensor:
668
+ """Average valid-voxel MSE within each scan, then average across scans."""
669
+ batch_ids, slot_ids = pred_token_mask.nonzero(as_tuple=True)
670
+ patch_ids = pred_ids[batch_ids, slot_ids]
671
+ targets = targets_patches[batch_ids, patch_ids]
672
+ voxel_mask = pred_mask_patches[batch_ids, patch_ids]
673
+
674
+ patch_errors = ((preds - targets) ** 2 * voxel_mask).sum(dim=1)
675
+ patch_voxels = voxel_mask.sum(dim=1).to(dtype=patch_errors.dtype)
676
+ batch_size = targets_patches.shape[0]
677
+ scan_errors = patch_errors.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_errors)
678
+ scan_voxels = patch_voxels.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_voxels)
679
+ return (scan_errors / scan_voxels).mean()
680
+
681
+ @torch.no_grad()
682
+ def forward_pred_images(
683
+ self,
684
+ preds: Float[Tensor, "B Q P"],
685
+ pred_ids: Int[Tensor, "B Q"],
686
+ pred_token_mask: Tensor | None = None,
687
+ img_mask: Tensor | None = None,
688
+ targets_stats: tuple[Tensor, Tensor] | None = None,
689
+ ) -> Tensor:
690
+ B, _, P = preds.shape
691
+ N = self.pred_patchify.num_patches
692
+ if pred_token_mask is not None:
693
+ preds = preds.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
694
+
695
+ preds = torch.zeros((B, N, P), dtype=preds.dtype, device=preds.device).scatter_add_(
696
+ 1, pred_ids.unsqueeze(-1).expand(-1, -1, P), preds
697
+ )
698
+
699
+ if targets_stats is not None:
700
+ targets_mean, targets_std = targets_stats
701
+ preds = preds * targets_std + targets_mean
702
+
703
+ pred_images = self.pred_patchify.unpatchify(preds)
704
+ if img_mask is not None:
705
+ pred_images = pred_images.masked_fill(~img_mask, 0)
706
+ return pred_images
707
+
708
+ def forward(
709
+ self,
710
+ images: Tensor,
711
+ img_mask: Tensor,
712
+ mask_ratio: float,
713
+ pred_mask_ratio: float | None = None,
714
+ pad_to_multiple: int | None = None,
715
+ with_state: bool = True,
716
+ ) -> Tensor | tuple[Tensor, dict]:
717
+ img_mask, visible_mask, pred_mask = self.prepare_masks(
718
+ img_mask,
719
+ None,
720
+ None,
721
+ device=images.device,
722
+ )
723
+ targets_patches, targets_stats = self.prepare_targets(images, img_mask)
724
+
725
+ (
726
+ cls_embeds,
727
+ reg_embeds,
728
+ patch_embeds,
729
+ visible_mask,
730
+ visible_ids,
731
+ visible_token_mask,
732
+ ) = self.encoder(
733
+ images,
734
+ mask=visible_mask,
735
+ mask_ratio=mask_ratio,
736
+ pad_to_multiple=pad_to_multiple,
737
+ )
738
+
739
+ pred_mask_patches, pred_ids, pred_token_mask = self.prepare_pred_mask(
740
+ visible_mask,
741
+ pred_mask=pred_mask,
742
+ pred_mask_ratio=pred_mask_ratio,
743
+ pad_to_multiple=pad_to_multiple,
744
+ )
745
+
746
+ preds = self.forward_decoder(
747
+ patch_embeds,
748
+ visible_ids,
749
+ pred_ids,
750
+ visible_token_mask=visible_token_mask,
751
+ pred_token_mask=pred_token_mask,
752
+ packed_output=not with_state,
753
+ )
754
+
755
+ loss_preds = preds if not with_state else preds[pred_token_mask]
756
+ loss = self.forward_loss(
757
+ loss_preds,
758
+ targets_patches,
759
+ pred_mask_patches,
760
+ pred_ids,
761
+ pred_token_mask,
762
+ )
763
+
764
+ if not with_state:
765
+ return loss
766
+
767
+ pred_mask = self.pred_patchify.unpatchify(pred_mask_patches)
768
+ pred_images = self.forward_pred_images(
769
+ preds,
770
+ pred_ids,
771
+ pred_token_mask=pred_token_mask,
772
+ img_mask=img_mask,
773
+ targets_stats=targets_stats,
774
+ )
775
+
776
+ state = {
777
+ "targets_patches": targets_patches,
778
+ "targets_stats": targets_stats,
779
+ "patch_embeds": patch_embeds,
780
+ "cls_embeds": cls_embeds,
781
+ "reg_embeds": reg_embeds,
782
+ "visible_mask": visible_mask,
783
+ "visible_ids": visible_ids,
784
+ "visible_token_mask": visible_token_mask,
785
+ "pred_mask": pred_mask,
786
+ "pred_ids": pred_ids,
787
+ "pred_token_mask": pred_token_mask,
788
+ "preds": preds,
789
+ "pred_images": pred_images,
790
+ }
791
+ return loss, state
792
+
793
+ def forward_embedding(
794
+ self,
795
+ x: Tensor,
796
+ mask: Tensor | None = None,
797
+ mask_ratio: float | None = None,
798
+ ):
799
+ return self.encoder.forward_embedding(x, mask=mask, mask_ratio=mask_ratio)
800
+
801
+
802
+ class MaskedViT(MaskedEncoder, PyTorchModelHubMixin):
803
+ def __init__(
804
+ self,
805
+ img_size: int | tuple[int, int, int] = (208, 240, 208),
806
+ in_chans: int = 1,
807
+ patch_size: int | tuple[int, int, int] = (16, 16, 16),
808
+ depth: int = 12,
809
+ embed_dim: int = 768,
810
+ num_heads: int = 12,
811
+ qkv_bias: bool = True,
812
+ proj_bias: bool = True,
813
+ mlp_ratio: int | float = 4,
814
+ class_token: bool = True,
815
+ reg_tokens: int = 0,
816
+ no_embed_class: bool = False,
817
+ final_norm: bool = True,
818
+ drop_path_rate: float = 0.0,
819
+ mask_drop_scale: bool = False,
820
+ pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
821
+ ):
822
+ img_size = _to_3d_tuple(img_size, "img_size")
823
+ patch_size = _to_3d_tuple(patch_size, "patch_size")
824
+
825
+ patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
826
+ patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
827
+ if pos_embed == "sincos":
828
+ pos_embed_layer = SinCosPosEmbed3D
829
+ else:
830
+ pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
831
+ pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
832
+
833
+ super().__init__(
834
+ patchify=patchify,
835
+ patch_embed=patch_embed,
836
+ pos_embed=pos_embed,
837
+ depth=depth,
838
+ embed_dim=embed_dim,
839
+ num_heads=num_heads,
840
+ qkv_bias=qkv_bias,
841
+ proj_bias=proj_bias,
842
+ mlp_ratio=mlp_ratio,
843
+ class_token=class_token,
844
+ reg_tokens=reg_tokens,
845
+ no_embed_class=no_embed_class,
846
+ final_norm=final_norm,
847
+ drop_path_rate=drop_path_rate,
848
+ mask_drop_scale=mask_drop_scale,
849
+ )
850
+
851
+ self.init_weights()
852
+
853
+ def init_weights(self):
854
+ self.apply(_init_weights)
855
+
856
+
857
+ def _to_3d_tuple(value: int | Sequence[int], name: str) -> tuple[int, int, int]:
858
+ if isinstance(value, int):
859
+ return (value, value, value)
860
+ if len(value) != 3:
861
+ raise ValueError(f"{name} must have exactly 3 spatial dimensions, got {tuple(value)}")
862
+ return tuple(int(item) for item in value)
863
+
864
+
865
+ # JAX ViT xavier uniform init
866
+ # https://github.com/facebookresearch/capi/blob/main/model.py
867
+ def _init_weights(m: nn.Module) -> None:
868
+ if isinstance(m, nn.Linear):
869
+ nn.init.xavier_uniform_(m.weight)
870
+ if m.bias is not None:
871
+ nn.init.constant_(m.bias, 0)
872
+ elif isinstance(m, nn.LayerNorm) and m.elementwise_affine:
873
+ nn.init.constant_(m.weight, 1.0)
874
+ if m.bias is not None:
875
+ nn.init.constant_(m.bias, 0)
876
+
877
+
878
+ def _create_vit(**kwargs):
879
+ model = MaskedViT(**kwargs)
880
+ return model
881
+
882
+
883
+ def _create_mae_vit(**kwargs):
884
+ model = MaskedAutoencoderViT(**kwargs)
885
+ return model
886
+
887
+
888
+ def mae_vit_small(**kwargs):
889
+ model_args = dict(embed_dim=384, depth=12, num_heads=6)
890
+ return _create_mae_vit(**model_args, **kwargs)
891
+
892
+
893
+ def mae_vit_base(**kwargs):
894
+ model_args = dict(embed_dim=768, depth=12, num_heads=12)
895
+ return _create_mae_vit(**model_args, **kwargs)
896
+
897
+
898
+ def mae_vit_large(**kwargs):
899
+ model_args = dict(embed_dim=1024, depth=24, num_heads=16)
900
+ return _create_mae_vit(**model_args, **kwargs)
901
+
902
+
903
+ def mae_vit_huge(**kwargs):
904
+ model_args = dict(embed_dim=1280, depth=32, num_heads=16)
905
+ return _create_mae_vit(**model_args, **kwargs)
906
+
907
+
908
+ # "patch embed" baseline model, depth 0 ViT (hah)
909
+ def patch_embed_small(**kwargs):
910
+ model_args = dict(embed_dim=384, depth=0)
911
+ return _create_vit(**model_args, **kwargs)
912
+
913
+
914
+ def patch_embed_base(**kwargs):
915
+ model_args = dict(embed_dim=768, depth=0)
916
+ return _create_vit(**model_args, **kwargs)
finetune/fomo_tune_baseline/output/task3/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task3
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task3
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/output/task3/log.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 16:34:12 run task3 (git 1df2e5d)
2
+ 16:34:12 config:
3
+ task: task3
4
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
5
+ output_root: experiments/fomo_tune_baseline/output
6
+ name: task3
7
+ device: cuda
8
+ seed: 4466
9
+ 16:35:16 dataset: 494 subjects, age 19-80 mean 45.2
10
+ 16:38:39 fold 1/20 n=25 mae=3.91 (190s)
11
+ 16:38:51 fold 2/20 n=25 mae=4.50 (202s)
12
+ 16:38:57 fold 3/20 n=25 mae=3.74 (208s)
13
+ 16:39:03 fold 4/20 n=25 mae=3.58 (214s)
14
+ 16:39:09 fold 5/20 n=25 mae=3.57 (220s)
15
+ 16:39:14 fold 6/20 n=25 mae=3.12 (226s)
16
+ 16:39:20 fold 7/20 n=25 mae=3.42 (232s)
17
+ 16:39:26 fold 8/20 n=25 mae=3.12 (238s)
18
+ 16:39:32 fold 9/20 n=25 mae=3.74 (244s)
19
+ 16:39:38 fold 10/20 n=25 mae=4.20 (249s)
20
+ 16:39:44 fold 11/20 n=25 mae=3.22 (255s)
21
+ 16:39:50 fold 12/20 n=25 mae=3.50 (261s)
22
+ 16:39:55 fold 13/20 n=25 mae=3.39 (267s)
23
+ 16:40:01 fold 14/20 n=25 mae=4.17 (273s)
24
+ 16:40:07 fold 15/20 n=24 mae=4.36 (278s)
25
+ 16:40:12 fold 16/20 n=24 mae=4.15 (284s)
26
+ 16:40:18 fold 17/20 n=24 mae=3.66 (289s)
27
+ 16:40:23 fold 18/20 n=24 mae=3.54 (295s)
28
+ 16:40:29 fold 19/20 n=24 mae=3.42 (300s)
29
+ 16:40:34 fold 20/20 n=24 mae=3.53 (306s)
30
+ 16:40:35 result: pearson_r=0.9631 pearson_r_ci_low=0.9567 pearson_r_ci_high=0.9687 mae=3.6910 mae_ci_low=3.4464 mae_ci_high=3.9466 (306s)
finetune/fomo_tune_baseline/output/task3/metrics.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name": "task3", "pearson_r": 0.9630771532969529, "pearson_r_ci_low": 0.9567144985968425, "pearson_r_ci_high": 0.9687044710309238, "mae": 3.6910024558001684, "mae_ci_low": 3.446356968454986, "mae_ci_high": 3.946628273643462, "run_time": 306.1}
finetune/fomo_tune_baseline/output/task3/model/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task3
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task3
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/output/task5/build/Apptainer.def ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Bootstrap: docker
2
+ From: python:3.11-slim
3
+
4
+ # NOT buildable where it sits: the %files paths below are relative to the build cwd, which is the
5
+ # staging dir `build.py` writes. Build it with `python -m fomo_tune.build <run_dir>`, not by
6
+ # pointing apptainer at this file.
7
+ #
8
+ # Versions are pinned to the training environment: numpy, scikit-learn and joblib because they
9
+ # unpickle `head.joblib`, torch because that is what the checkpoint was written by.
10
+
11
+ %files
12
+ fomo_tune /app/fomo_tune
13
+ smri_mae /app/smri_mae
14
+ model /app/model
15
+ predict.py /app/predict.py
16
+
17
+ %post
18
+ pip install --no-cache-dir \
19
+ torch==2.8.0 \
20
+ numpy==2.4.6 \
21
+ nibabel==5.4.2 \
22
+ einops==0.8.2 \
23
+ jaxtyping==0.3.10 \
24
+ timm==1.0.27 \
25
+ huggingface-hub==0.36.2 \
26
+ scikit-learn==1.8.0 \
27
+ joblib==1.5.3 \
28
+ omegaconf==2.3.0
29
+
30
+ %runscript
31
+ exec python /app/predict.py "$@"
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/README.md ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fomo_tune
2
+
3
+ The five FOMO26 challenge tasks, one script each, tuned independently.
4
+
5
+ This is a spinoff of `nanobrain.eval`, which scored every backbone on every task through one fixed
6
+ probe. That was the right shape for a benchmark and the wrong shape for a competition: here we care
7
+ about one backbone (sMRI MAE) and five scores, and each task wants a different method. **Nothing
8
+ here imports `nanobrain.eval`, and it should stay that way** — this package may be shared with
9
+ people who won't get the eval suite.
10
+
11
+ ## Layout
12
+
13
+ | File | |
14
+ |---|---|
15
+ | `datasets.py` | core, **frozen**. One `load_fomo_task<k>()` per task, streaming the challenge zips into an HF dataset. Raw niftis, no resampling — the backbone transform does that. |
16
+ | `backbone.py` | core, **frozen**. `load_backbone(ckpt_path) -> (SmriMaeBackbone, SmriMaeTransform)`. Frozen sMRI MAE encoder; the transform canonicalizes to RAS, rescales to 1mm, fits to the pretraining shape, z-scores in a mean-threshold brain mask. |
17
+ | `utils.py` | core. `set_seed`, `git_sha`, `setup_logging`. |
18
+ | `main_task<k>.py` | shell. One task, end to end. Task 1 is the worked example; copy it. |
19
+ | `build.py` + `Apptainer.def` | shell. Package a run dir into the challenge `.sif`. Shared by every task. |
20
+
21
+ `datasets.py` and `backbone.py` are settled and their caches are warm. Treat them as read-only:
22
+ new work goes in `main_task<k>.py`. If one of them genuinely needs to change, that is a
23
+ conversation first, because it invalidates every score already recorded.
24
+
25
+ ## The pattern
26
+
27
+ `main_task1.py` is in three sections, and the split is the point of the whole design.
28
+
29
+ **`Task1Method` — the part we tune.** Features, head, hyperparameters, anything that might move
30
+ the score. Its interface is:
31
+
32
+ ```python
33
+ method.fit(rows) # rows are dataset records: subject, label, images
34
+ method.predict(images) # -> the challenge's output for one subject
35
+ method.save(model_dir) # config.yaml + head.joblib
36
+ Task1Method.load(model_dir, **overrides)
37
+ ```
38
+
39
+ **The protocol — fixed.** Pool out-of-fold predictions over all subjects, bootstrap subjects for
40
+ the CI. No repeats, no stratification; the bootstrap is the only variance estimate. Splitting is
41
+ per-task but fixed within a task — leave-one-out where n is tiny (task 1, n=21), **20-fold** where
42
+ it isn't (tasks 3 and 5), which is close enough to LOO without paying for 494 refits. Once a task's
43
+ scheme is set, hold it or scores stop being comparable across iterations. That is also why
44
+ `cross_validate` seeds its shuffle at 0 rather than from `cfg.seed`: the folds are part of the
45
+ protocol, so tuning the run's seed must not silently redraw them.
46
+
47
+ **Two entrypoints.** `train` runs the protocol then fits a head on all subjects and saves it;
48
+ `predict` is the challenge CLI. Both go through `Method.predict`, which is why every fold
49
+ exercises the code the submission will run.
50
+
51
+ That last point is the load-bearing one. `predict` is not a wrapper written at packaging time — it
52
+ is the same call cross-validation already made once per held-out subject. When you add a task, keep
53
+ that property.
54
+
55
+ ```bash
56
+ uv run python -m fomo_tune.main_task1 train modalities=[dwi_b1000,flair] name=task1_dwi_flair
57
+ uv run python -m fomo_tune.main_task1 predict --model-dir output/fomo_tune/task1_dwi/model \
58
+ --adc adc.nii.gz --dwi dwi.nii.gz --flair flair.nii.gz --output prob.txt
59
+ ```
60
+
61
+ `train` takes omegaconf dotlist overrides against the `Config` dataclass at the top of the file.
62
+ It writes `config.yaml`, `log.txt`, `metrics.json`, and `model/` into `{output_root}/{name}/`.
63
+
64
+ ## Status
65
+
66
+ Tasks 1, 5 and 3 are drafted and verified. Task 1 is also packaged — its container passes the
67
+ challenge validator; 5 and 3 have not been built yet. **Tasks 2 and 4 are tabled** — both are
68
+ segmentation, both need `predict` to emit a nifti on the input grid, and neither is worth opening
69
+ until the classification and regression tasks are settled.
70
+
71
+ All three on `vitl_fomo300`, one H100, wall being the cross-validation loop:
72
+
73
+ | run | result | wall |
74
+ |---|---|---|
75
+ | `task1_dwi`, dwi_b1000, n=21, LOO | AUROC **0.990** [0.944, 1.000] | 25s |
76
+ | `task5_t1w`, t1w, n=48, 20-fold | AUROC **0.984** [0.953, 1.000] | 73s |
77
+ | `task3_t1w`, t1w, n=494, 20-fold | r **0.962** [0.956, 0.968], MAE **3.71y** [3.45, 3.97] | 260s |
78
+
79
+ **Task 3's row is one fold-seed stale.** It was measured before `cross_validate` froze its shuffle
80
+ at 0, so it is a 20-fold run with `random_state=4466`. Task 1 (LOO) and task 5 are current. The
81
+ re-run is cheap — 260s on a GPU — it just has not been done. Expect a shift of the same order task
82
+ 5 saw when its folds moved (0.948 → 0.984, i.e. inside the CI but not negligible).
83
+
84
+ Task 1's earlier probe sweep got 0.954 [0.861, 1.000] on the same checkpoint
85
+ (`experiments/eval_global_0728`), so it roughly reproduces — the gap is LOO vs 5×5 stratified CV,
86
+ one interpolation instead of two, and a head selected on AUROC instead of balanced accuracy.
87
+
88
+ Two checks worth repeating per task — `.claude/scratch/verify_task1.py` and
89
+ `.claude/scratch/verify_task35.py <k>` do both:
90
+ - features are **bit-identical** whether the nifti comes from the HF dataset wrapper or from
91
+ `nib.load` off disk, so CV numbers transfer to the container
92
+ - the `predict` CLI agrees with the in-process method
93
+
94
+ ## What changes per task
95
+
96
+ Counts and modalities, read from the local zips:
97
+
98
+ | Task | n | Inputs | Output | Split | Notes |
99
+ |---|---|---|---|---|---|
100
+ | 1 infarct | 21 | adc, dwi_b1000, flair (+t2s/swi) | probability | LOO | done |
101
+ | 5 polymicrogyria | 48 | t1w | probability | 20-fold | done |
102
+ | 3 brain age | 494 | t1w | age in years | 20-fold | done — RidgeCV head, scored by **Pearson r and MAE**, each with its own bootstrap CI |
103
+ | 2 meningioma | 23 | dwi_b1000, flair (+t2s/swi) | mask, input grid | — | tabled |
104
+ | 4 trigeminal | 40 | t2w | mask, labels 1=nerve 2=vessel | — | tabled |
105
+
106
+ Tasks 5 and 3 diverge from task 1 only where that table says. `cross_validate` over a shuffled
107
+ `KFold` replaces `leave_one_out`; both take one modality, so `features` loses the
108
+ concat-over-modalities loop and `Config` loses `modalities`; the challenge CLI flag is `--t1` for
109
+ both, and it is `--t1` for task 3 too even though the file in the zip is `t1w.nii.gz`.
110
+
111
+ Task 3 is the first regression, so its `score` loops over the two metrics rather than returning
112
+ one, and it drops task 1's guard against bootstrap resamples with fewer than two distinct labels.
113
+ The analogous degenerate case for regression is a resample with no spread in `y`, where Pearson r
114
+ is undefined rather than merely unstable — at n=494 it does not happen.
115
+
116
+ When tasks 2 and 4 come back: `predict` must write a nifti on the input's grid, and the method
117
+ needs localized features rather than a pooled vector — `backbone.forward` returns `patch_coords`
118
+ in world mm for exactly that. Task 4's label order (1=nerve, 2=vessel) is still a guess and needs
119
+ confirming against the challenge data before per-class numbers mean anything.
120
+
121
+ ## Gotchas
122
+
123
+ **Raw niftis are on disk** at `data/fomo_eval/Task_<k>/preprocessed/<sub>/ses-01/`, which is the
124
+ easy way to exercise `predict` on a real file rather than one written out of the dataset:
125
+
126
+ ```bash
127
+ uv run python -m fomo_tune.main_task1 predict \
128
+ --model-dir output/fomo_tune/task1_dwi/model \
129
+ --adc data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/adc.nii.gz \
130
+ --dwi data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/dwi_b1000.nii.gz \
131
+ --flair data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/flair.nii.gz \
132
+ --output /tmp/prob.txt
133
+ ```
134
+
135
+ Task 5 breaks the naming: `Task_5/preprocessed/sub_01/ses_01/t1.nii.gz` — underscores throughout,
136
+ and `t1` not `t1w`. `datasets.py` already handles it; anything you write by hand won't.
137
+
138
+ ```bash
139
+ uv run python -m fomo_tune.main_task5 predict --model-dir output/fomo_tune/task5_t1w/model \
140
+ --t1 data/fomo_eval/Task_5/preprocessed/sub_01/ses_01/t1.nii.gz --output /tmp/prob.txt
141
+ ```
142
+
143
+ **Volumes are wildly anisotropic.** Task 1's DWI is 0.46×0.46×**5.6**mm, so the transform
144
+ upsamples z by 5.6× to reach 1mm iso. Nothing is wrong, but don't read the 1mm grid as real
145
+ resolution.
146
+
147
+ **The backbone never saw skull or neck.** Pretraining used a SynthSeg brain mask; the transform
148
+ substitutes a mean-intensity threshold, which keeps both. Known fidelity gap — see
149
+ `.claude/memory/smri-mae-preprocessing-gap.md`.
150
+
151
+ **Probabilities are not calibrated.** `LogisticRegressionCV` on ~20 samples × 1024 features shrinks
152
+ hard; task 1's out-of-fold probabilities all land in 0.48–0.52 with near-perfect ranking. Fine for
153
+ AUROC, which is what the challenge scores, but don't read them as probabilities. Task 5's do span
154
+ 0–1, which is n=48 rather than n=21 and not evidence of calibration.
155
+
156
+ **n is tiny, so the CI is the result.** Task 1's is ~0.06 wide at the top of the range. Most tuning
157
+ deltas you chase will be inside it. `.claude/NOTES.md` thread 1 has the longer argument.
158
+
159
+ **GPUs need an allocation** — the login node has no driver. See the `gpu-session` skill.
160
+
161
+ ## Submission
162
+
163
+ `build.py` packages a run dir into the `.sif` the challenge wants. One command, taking the run dir
164
+ the shipped head was saved into:
165
+
166
+ ```bash
167
+ uv run python -m fomo_tune.build output/fomo_tune/task1_dwi
168
+ ```
169
+
170
+ It stages `/app`, then builds from there:
171
+
172
+ ```
173
+ /app/predict.py # shim: calls fomo_tune.main_task<k> predict
174
+ /app/model/config.yaml # from the run dir
175
+ /app/model/head.joblib # from the run dir
176
+ /app/model/backbone.pth # stripped checkpoint, --ckpt-path points here
177
+ ```
178
+
179
+ **Both `build.py` and `Apptainer.def` are shared across tasks**, which they can be because nothing
180
+ in staging or in the dependency list is task-specific. The one thing that does vary is the module
181
+ the shim imports, and that comes from `task` in the run's saved config — so a run dir knows which
182
+ task it belongs to, and `build.py` never needs telling.
183
+
184
+ `predict.py` is **generated at build time** rather than checked in. It is eight lines whose whole
185
+ meaning is the container layout staged around it, so there is nowhere outside a container to run
186
+ it. This does not weaken the point above about `predict` not being written at packaging time: the
187
+ logic still lives in `main_task<k>.py`, exercised once per fold, and the shim only picks the
188
+ subcommand and two paths.
189
+
190
+ **`Apptainer.def` is not buildable where it sits.** Its `%files` paths are relative to the build
191
+ cwd, which is the staging dir. Pointing `apptainer build` at it in the repo fails confusingly; go
192
+ through `build.py`.
193
+
194
+ The run dir deliberately does *not* carry backbone weights — that checkpoint is 3.9G and would be
195
+ copied on every run. `--ckpt-path` overrides what `config.yaml` recorded, so the saved config stays
196
+ a faithful record of what trained rather than being rewritten at build time.
197
+
198
+ **The staged checkpoint is stripped to `model` and `args`**, which is 3.9G → 1.3G because the rest
199
+ is optimizer state inference never reads. `load_backbone` needs no change for this, and `predict`
200
+ gives a bit-identical probability either way (0.524739 on `sub-20`, checked on GPU).
201
+
202
+ **The base image is `python:3.11-slim`, not a CUDA image.** The PyPI torch wheel *is* the cu128
203
+ build and vendors the whole CUDA userspace as `nvidia-*` packages, so all the container needs from
204
+ the host is the driver, which `--nv`/`--nvccli` binds in. That keeps the SIF at 5.3G (4.0G of
205
+ image, 1.3G of weights) against roughly double for `pytorch/pytorch` and far more for NGC.
206
+ Versions are pinned to the training environment
207
+ mostly so `head.joblib` unpickles against the numpy/sklearn that wrote it.
208
+
209
+ Apptainer caches the bootstrap layers but **always re-runs `%post`**, so every build re-downloads
210
+ ~3G of wheels. If that gets annoying, bake a deps-only base SIF and `Bootstrap: localimage` off it.
211
+
212
+ ### Validating
213
+
214
+ `third_party/container-validator` is the challenge's own validator, test niftis included:
215
+
216
+ ```bash
217
+ python third_party/container-validator/container_validator/validate.py \
218
+ --task task1 --sif output/fomo_tune/task1_dwi/task1.sif
219
+ ```
220
+
221
+ It runs `python /app/predict.py --flair /input/… --adc … --dwi … --swi … --output /output/<sid>.txt`
222
+ inside an `apptainer instance` with `/input`, `/output` and `/tmp` bound — which is exactly the
223
+ shim's contract, so nothing in `predict.py` is guessing at the interface.
224
+
225
+ One thing it does that is easy to miss: it takes GPU via `--nvccli` rather than `--nv`, and one of
226
+ its tests runs `nvidia-smi -L` **inside** the container. `python:3.11-slim` ships no `nvidia-smi`,
227
+ so that test passes only because `--nvccli` injects the host one — a CUDA base image would hide
228
+ that dependency rather than remove it.
229
+
230
+ **The `task1_dwi` container passes all 20 validator tests**, and `predict` inside it returns
231
+ 0.524739 on `sub-20`, identical to the same call outside the container. So the packaging is
232
+ verified end to end, not just built.
233
+
234
+ **Run it on a compute node with apptainer, which as of 2026-08-11 means `n-6`** — `salloc
235
+ --nodelist=n-6`. The other nodes fail the validator's preflight. The login node has apptainer but
236
+ no driver, and
237
+ `predict` there dies inside `can_use_cudnn_attention` — the jagged-SDPA path reaches into CUDA even
238
+ when the tensors are on CPU, so a driver-less host fails at the forward pass rather than falling
239
+ back. That is the CPU gap worth remembering; it is not a container problem.
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/backbone.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ import nibabel as nib
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from einops import rearrange
9
+ from torch import Tensor
10
+
11
+ import smri_mae.model_mae as models_mae
12
+
13
+
14
+ class SmriMaeBackbone(nn.Module):
15
+ grid_coords: Tensor
16
+
17
+ def __init__(self, encoder: models_mae.MaskedEncoder):
18
+ super().__init__()
19
+ self.encoder = encoder
20
+ self.img_size = self.encoder.patchify.img_size
21
+
22
+ grid_size = self.encoder.patchify.grid_size
23
+ patch_size = np.array(self.encoder.patchify.patch_size)
24
+ grid_coords = rearrange(np.indices(grid_size), "c x y z -> (x y z) c")
25
+ grid_coords = grid_coords * patch_size + (patch_size - 1) / 2
26
+ grid_coords = torch.as_tensor(grid_coords, dtype=torch.float32)
27
+ self.register_buffer("grid_coords", grid_coords)
28
+
29
+ def forward(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
30
+ images = batch["image"]
31
+ mask = batch["mask"]
32
+ affine = batch["affine"]
33
+
34
+ B, C, X, Y, Z = images.shape
35
+ assert (X, Y, Z) == self.img_size, f"expected {self.img_size}, got {(X, Y, Z)}"
36
+
37
+ _, _, patch_embeds, _, patch_ids, token_mask = self.encoder(images, mask=mask)
38
+
39
+ # [B, L, 3] world xyz coords of embeddings
40
+ patch_coords = self.grid_coords[patch_ids, :]
41
+ rot = affine[:, :3, :3]
42
+ trans = affine[:, :3, 3]
43
+ patch_coords = patch_coords @ rot.transpose(1, 2) + trans[:, None, :]
44
+
45
+ return {
46
+ "patch_embeds": patch_embeds,
47
+ "patch_ids": patch_ids,
48
+ "token_mask": token_mask,
49
+ "patch_coords": patch_coords,
50
+ }
51
+
52
+
53
+ class SmriMaeTransform:
54
+ def __init__(
55
+ self,
56
+ img_size: tuple[int, int, int] = (208, 240, 208),
57
+ spacing: tuple[float, float, float] = (1.0, 1.0, 1.0),
58
+ ):
59
+ self.img_size = img_size
60
+ self.spacing = spacing
61
+
62
+ def __call__(self, img: nib.Nifti1Image) -> dict[str, Tensor]:
63
+ # repack image to handle incomplete hf Nifti interface
64
+ img = nib.Nifti1Image(img.dataobj, img.affine, img.header)
65
+ img = nib.as_closest_canonical(img)
66
+
67
+ data = torch.from_numpy(np.ascontiguousarray(img.get_fdata(dtype=np.float32)))
68
+ affine = np.asarray(img.affine)
69
+
70
+ spacing = img.header.get_zooms()
71
+ if max(abs(s - s_) for s, s_ in zip(spacing, self.spacing)) > 0.05:
72
+ data, affine = rescale(data, affine, spacing, self.spacing)
73
+
74
+ data, affine = fit_to_shape(data, affine, target_shape=self.img_size)
75
+
76
+ # mean threshold, not the SynthSeg mask used in pretraining, so skull and neck stay in
77
+ mask = data > data.mean()
78
+ brain = data[mask]
79
+ mean = brain.mean()
80
+ # population std (correction=0) to match the pretraining normalization
81
+ std = brain.std(correction=0).clamp_min(1e-6)
82
+ data = torch.where(mask, (data - mean) / std, 0.0)
83
+
84
+ return {
85
+ "image": data.unsqueeze(0),
86
+ "mask": mask.unsqueeze(0),
87
+ "affine": torch.as_tensor(affine, dtype=torch.float32),
88
+ }
89
+
90
+
91
+ def rescale(
92
+ x: torch.Tensor,
93
+ affine: np.ndarray,
94
+ spacing: tuple[float, ...],
95
+ target_spacing: tuple[float, ...] = (1.0, 1.0, 1.0),
96
+ ) -> tuple[torch.Tensor, np.ndarray]:
97
+ scales = tuple([current / target for current, target in zip(spacing, target_spacing)])
98
+ resampled = F.interpolate(x[None, None], scale_factor=scales, mode="trilinear").squeeze(0, 1)
99
+
100
+ # align_corners=False reads output voxel j from input voxel (j + 0.5) / scale - 0.5
101
+ scale = np.asarray(scales, dtype=float)
102
+ step = np.diag([*(1 / scale), 1.0])
103
+ step[:3, 3] = 0.5 / scale - 0.5
104
+ return resampled, affine @ step
105
+
106
+
107
+ def fit_to_shape(
108
+ x: torch.Tensor, affine: np.ndarray, target_shape: tuple[int, ...]
109
+ ) -> tuple[torch.Tensor, np.ndarray]:
110
+ """Centre the volume in `target_shape`, padding the short axes and cropping the long ones."""
111
+ pads = [target - size for size, target in zip(x.shape, target_shape)]
112
+ padding = [side for pad in reversed(pads) for side in (pad // 2, pad - pad // 2)]
113
+
114
+ # a crop is a negative pad, so output voxel k came from input voxel k - pad // 2 either way
115
+ step = np.eye(4)
116
+ step[:3, 3] = [-(pad // 2) for pad in pads]
117
+ return F.pad(x, padding), affine @ step
118
+
119
+
120
+ def resolve_ckpt(ckpt_path: str) -> str:
121
+ """A local path for a checkpoint, downloading it if it is an hf://<org>/<repo>/<file> URI."""
122
+ from huggingface_hub import hf_hub_download
123
+
124
+ if ckpt_path.startswith("hf://"):
125
+ org, repo, *rest = ckpt_path.removeprefix("hf://").split("/")
126
+ return hf_hub_download(f"{org}/{repo}", "/".join(rest))
127
+
128
+ return ckpt_path
129
+
130
+
131
+ def load_backbone(ckpt_path: str) -> tuple[SmriMaeBackbone, SmriMaeTransform]:
132
+ path = resolve_ckpt(ckpt_path)
133
+ ckpt = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
134
+ args = ckpt["args"]
135
+
136
+ model_fn = models_mae.__dict__[args["model"]]
137
+ model: models_mae.MaskedAutoencoderViT = model_fn(
138
+ img_size=args["img_size"],
139
+ in_chans=args.get("in_chans", 1),
140
+ patch_size=args["patch_size"],
141
+ # older checkpoints carry training flags the current model_mae no longer takes
142
+ **filter_kwargs(models_mae.MaskedAutoencoderViT, args.get("model_kwargs") or {}),
143
+ )
144
+ model.load_state_dict(ckpt["model"])
145
+ backbone = SmriMaeBackbone(model.encoder)
146
+ transform = SmriMaeTransform(img_size=args["img_size"])
147
+ return backbone, transform
148
+
149
+
150
+ def filter_kwargs(func, kwargs):
151
+ signature = inspect.signature(func)
152
+ kwargs = {k: v for k, v in kwargs.items() if k in signature.parameters}
153
+ return kwargs
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/datasets.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ import zipfile
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+
9
+ import fsspec
10
+ from datasets import Dataset, Features, Nifti, Value
11
+
12
+ FOMO_EVAL_BASE_URL = os.getenv(
13
+ "FOMO_EVAL_BASE_URL",
14
+ "https://sid.erda.dk/share_redirect/fmeuvo1EdF",
15
+ )
16
+ FOMO_EVAL_TASK5_URL = os.getenv(
17
+ "FOMO_EVAL_TASK5_URL",
18
+ "https://huggingface.co/datasets/medarc/smri-fm/resolve/main/fomo_eval/Task_5.zip",
19
+ )
20
+
21
+
22
+ @contextmanager
23
+ def open_zip(url: str) -> Generator[zipfile.ZipFile, None, None]:
24
+ """Open a task zip, copying a remote url to a temp file first."""
25
+ with tempfile.TemporaryDirectory() as tmp:
26
+ local = Path(url)
27
+ if not local.exists():
28
+ local = Path(tmp) / "task.zip"
29
+ with fsspec.open(url) as src, local.open("wb") as dst:
30
+ shutil.copyfileobj(src, dst)
31
+ with zipfile.ZipFile(local) as zf:
32
+ yield zf
33
+
34
+
35
+ def subject_ids(zf: zipfile.ZipFile) -> list[str]:
36
+ return sorted({name.split("/")[2] for name in zf.namelist() if name.endswith(".nii.gz")})
37
+
38
+
39
+ # ---- Task 1: acute infarct (classification; positives also carry a lesion mask) --------
40
+
41
+
42
+ def load_fomo_task1() -> Dataset:
43
+ # No 4th modality: it is swi on 16 subjects and t2s on the other 5.
44
+ suffixes = ("adc", "dwi_b1000", "flair")
45
+ features = Features(
46
+ {
47
+ "subject": Value("string"),
48
+ "label": Value("int32"),
49
+ **{suffix: Nifti() for suffix in suffixes},
50
+ }
51
+ )
52
+ dataset = Dataset.from_generator(
53
+ _fomo_task1_generator,
54
+ features=features,
55
+ gen_kwargs={"suffixes": suffixes},
56
+ writer_batch_size=16,
57
+ )
58
+ return dataset
59
+
60
+
61
+ def _fomo_task1_generator(suffixes: tuple[str, ...]):
62
+ url = f"{FOMO_EVAL_BASE_URL}/Task_1.zip"
63
+ with open_zip(url) as zf:
64
+ for sub in subject_ids(zf):
65
+ label = int(zf.read(f"Task_1/labels/{sub}/ses-01/label.txt").strip())
66
+ sample = {"subject": sub, "label": label}
67
+ for suffix in suffixes:
68
+ name = f"Task_1/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
69
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
70
+ yield sample
71
+
72
+
73
+ # ---- Task 2: meningioma segmentation ---------------------------------------------------
74
+
75
+
76
+ def load_fomo_task2() -> Dataset:
77
+ # No 4th modality: it is t2s on 15 subjects and swi on the other 8.
78
+ suffixes = ("dwi_b1000", "flair")
79
+ features = Features(
80
+ {
81
+ "subject": Value("string"),
82
+ **{suffix: Nifti() for suffix in suffixes},
83
+ "seg": Nifti(),
84
+ }
85
+ )
86
+ dataset = Dataset.from_generator(
87
+ _fomo_task2_generator,
88
+ features=features,
89
+ gen_kwargs={"suffixes": suffixes},
90
+ writer_batch_size=16,
91
+ )
92
+ return dataset
93
+
94
+
95
+ def _fomo_task2_generator(suffixes: tuple[str, ...]):
96
+ url = f"{FOMO_EVAL_BASE_URL}/Task_2.zip"
97
+ with open_zip(url) as zf:
98
+ for sub in subject_ids(zf):
99
+ sample = {"subject": sub}
100
+ for suffix in suffixes:
101
+ name = f"Task_2/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
102
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
103
+ # Seg is on the image grid (shapes match) but its affine differs by up to 0.03mm.
104
+ name = f"Task_2/labels/{sub}/ses-01/seg.nii.gz"
105
+ sample["seg"] = {"path": None, "bytes": zf.read(name)}
106
+ yield sample
107
+
108
+
109
+ # ---- Task 3: brain age regression ------------------------------------------------------
110
+
111
+
112
+ def load_fomo_task3() -> Dataset:
113
+ features = Features(
114
+ {
115
+ "subject": Value("string"),
116
+ "age": Value("int32"),
117
+ "t1w": Nifti(),
118
+ }
119
+ )
120
+ dataset = Dataset.from_generator(
121
+ _fomo_task3_generator,
122
+ features=features,
123
+ writer_batch_size=16,
124
+ )
125
+ return dataset
126
+
127
+
128
+ def _fomo_task3_generator():
129
+ url = f"{FOMO_EVAL_BASE_URL}/Task_3.zip"
130
+ with open_zip(url) as zf:
131
+ for sub in subject_ids(zf):
132
+ age = int(zf.read(f"Task_3/labels/{sub}/ses-01/labels.txt").strip())
133
+ image_gz = zf.read(f"Task_3/preprocessed/{sub}/ses-01/t1w.nii.gz")
134
+ sample = {
135
+ "subject": sub,
136
+ "age": age,
137
+ "t1w": {"path": None, "bytes": image_gz},
138
+ }
139
+ yield sample
140
+
141
+
142
+ # ---- Task 4: trigeminal nerve/vessel segmentation --------------------------------------
143
+
144
+
145
+ def load_fomo_task4() -> Dataset:
146
+ # Volumes are uncropped 0.5mm near-iso, ~360x512x512; crop before feeding a model.
147
+ features = Features(
148
+ {
149
+ "subject": Value("string"),
150
+ "t2w": Nifti(),
151
+ "seg": Nifti(),
152
+ }
153
+ )
154
+ dataset = Dataset.from_generator(
155
+ _fomo_task4_generator,
156
+ features=features,
157
+ writer_batch_size=16,
158
+ )
159
+ return dataset
160
+
161
+
162
+ def _fomo_task4_generator():
163
+ url = f"{FOMO_EVAL_BASE_URL}/Task_4.zip"
164
+ with open_zip(url) as zf:
165
+ for sub in subject_ids(zf):
166
+ image_gz = zf.read(f"Task_4/preprocessed/{sub}/ses-01/t2w.nii.gz")
167
+ seg_gz = zf.read(f"Task_4/labels/{sub}/ses-01/seg.nii.gz")
168
+ sample = {
169
+ "subject": sub,
170
+ "t2w": {"path": None, "bytes": image_gz},
171
+ "seg": {"path": None, "bytes": seg_gz},
172
+ }
173
+ yield sample
174
+
175
+
176
+ # ---- Task 5: polymicrogyria classification ---------------------------------------------
177
+
178
+
179
+ def load_fomo_task5() -> Dataset:
180
+ features = Features(
181
+ {
182
+ "subject": Value("string"),
183
+ "label": Value("int32"),
184
+ "t1w": Nifti(),
185
+ }
186
+ )
187
+ dataset = Dataset.from_generator(
188
+ _fomo_task5_generator,
189
+ features=features,
190
+ writer_batch_size=16,
191
+ )
192
+ return dataset
193
+
194
+
195
+ def _fomo_task5_generator():
196
+ with open_zip(FOMO_EVAL_TASK5_URL) as zf:
197
+ for sub in subject_ids(zf):
198
+ label = int(zf.read(f"Task_5/labels/{sub}/ses_01/labels.txt").strip())
199
+ image_gz = zf.read(f"Task_5/preprocessed/{sub}/ses_01/t1.nii.gz")
200
+ sample = {
201
+ "subject": sub,
202
+ "label": label,
203
+ "t1w": {"path": None, "bytes": image_gz},
204
+ }
205
+ yield sample
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task1.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 1: acute infarct classification, scored by AUROC as the challenge scores it.
2
+
3
+ `Task1Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: leave one subject out, pool the out-of-fold
5
+ predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract,
8
+ modality paths in and one probability out. Both go through `Task1Method.predict`, so every fold
9
+ exercises the path the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import LogisticRegressionCV
25
+ from sklearn.metrics import roc_auc_score
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task1"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ modalities: list[str] = field(default_factory=lambda: ["dwi_b1000"])
44
+ output_root: str = "output/fomo_tune"
45
+ name: str = "task1"
46
+ device: str = "cuda"
47
+ seed: int = 4466
48
+
49
+
50
+ # ---- method: the part we tune -----------------------------------------------------------
51
+
52
+
53
+ class Task1Method:
54
+ """Frozen sMRI MAE, mean-pooled tokens per modality concatenated, logistic head."""
55
+
56
+ def __init__(self, cfg: Config):
57
+ self.cfg = cfg
58
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
59
+ self.device = torch.device(cfg.device)
60
+ self.backbone.to(self.device).eval().requires_grad_(False)
61
+ self.modalities = list(cfg.modalities)
62
+ self.cache: dict[str, np.ndarray] = {}
63
+ self.head = None
64
+
65
+ @torch.inference_mode()
66
+ def features(self, images: Images) -> np.ndarray:
67
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
68
+ pooled = []
69
+ for modality in self.modalities:
70
+ sample = self.transform(images[modality])
71
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
72
+
73
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
74
+ out = self.backbone(batch)
75
+
76
+ patch_embeds = out["patch_embeds"]
77
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
78
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
79
+ pooled.append(embed[0].float().cpu())
80
+
81
+ return torch.cat(pooled).numpy()
82
+
83
+ def cached_features(self, row: dict) -> np.ndarray:
84
+ if row["subject"] not in self.cache:
85
+ self.cache[row["subject"]] = self.features(row)
86
+ return self.cache[row["subject"]]
87
+
88
+ def fit(self, rows: list[dict]) -> None:
89
+ X = np.stack([self.cached_features(row) for row in rows])
90
+ y = np.array([row["label"] for row in rows])
91
+
92
+ clf = LogisticRegressionCV(
93
+ Cs=10,
94
+ class_weight="balanced",
95
+ scoring="roc_auc",
96
+ max_iter=1000,
97
+ l1_ratios=(0,),
98
+ use_legacy_attributes=False,
99
+ )
100
+ self.head = make_pipeline(StandardScaler(), clf)
101
+ self.head.fit(X, y)
102
+ self.positive = list(self.head.classes_).index(1)
103
+
104
+ def predict(self, images: Images) -> float:
105
+ """Positive-class probability. Indexes `classes_` rather than assuming column 1, which
106
+ would silently score the wrong class if the label order differed."""
107
+ X = self.features(images)[None]
108
+ probs = self.head.predict_proba(X)[0]
109
+ return float(probs[self.positive])
110
+
111
+ def save(self, model_dir: Path) -> None:
112
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
113
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
114
+ model_dir.mkdir(parents=True, exist_ok=True)
115
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
116
+ joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
117
+
118
+ @classmethod
119
+ def load(cls, model_dir: Path, **overrides) -> "Task1Method":
120
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
121
+ between here and the container -- the backbone path, the device."""
122
+ cfg = OmegaConf.merge(
123
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
124
+ )
125
+ method = cls(cfg)
126
+ state = joblib.load(model_dir / "head.joblib")
127
+ method.head, method.positive = state["head"], state["positive"]
128
+ return method
129
+
130
+
131
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
132
+
133
+ # Every image the task ships. The method picks which of them it wants, as at inference, where
134
+ # the challenge hands over all four modalities whether or not a model uses them.
135
+ IMAGE_COLS = ("adc", "dwi_b1000", "flair")
136
+
137
+
138
+ def leave_one_out(rows: list[dict], method: Task1Method) -> tuple[np.ndarray, np.ndarray]:
139
+ """Out-of-fold score for every subject, each predicted by a head fit on the other n-1."""
140
+ y = np.array([row["label"] for row in rows])
141
+ oof = np.zeros(len(rows), dtype=float)
142
+ start = time.perf_counter()
143
+ for held_out, row in enumerate(rows):
144
+ method.fit([r for r in rows if r["subject"] != row["subject"]])
145
+ oof[held_out] = method.predict({key: row[key] for key in IMAGE_COLS})
146
+ logger.info(
147
+ f"fold {held_out + 1}/{len(rows)} {row['subject']} "
148
+ f"y={y[held_out]} p={oof[held_out]:.3f} ({time.perf_counter() - start:.0f}s)"
149
+ )
150
+ return y, oof
151
+
152
+
153
+ def score(
154
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
155
+ ) -> dict:
156
+ """AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
157
+ rng = np.random.default_rng(seed)
158
+ samples = []
159
+ for _ in range(n_boot):
160
+ rows = rng.integers(0, len(y), size=len(y))
161
+ if len(np.unique(y[rows])) < 2:
162
+ continue
163
+ samples.append(roc_auc_score(y[rows], oof[rows]))
164
+
165
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
166
+ return {
167
+ "auroc": float(roc_auc_score(y, oof)),
168
+ "auroc_ci_low": float(low),
169
+ "auroc_ci_high": float(high),
170
+ }
171
+
172
+
173
+ # ---- entrypoints ------------------------------------------------------------------------
174
+
175
+
176
+ def train(args: argparse.Namespace) -> None:
177
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
178
+ from fomo_tune.datasets import load_fomo_task1
179
+
180
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
181
+ run_dir = Path(cfg.output_root) / cfg.name
182
+ run_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ setup_logging(run_dir)
185
+ set_seed(cfg.seed)
186
+ logger.info(f"run {cfg.name} (git {git_sha()})")
187
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
188
+ OmegaConf.save(cfg, run_dir / "config.yaml")
189
+
190
+ # decoded once: leave-one-out revisits every subject n times, and the niftis are small
191
+ rows = list(load_fomo_task1())
192
+ logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
193
+
194
+ method = Task1Method(cfg)
195
+ start = time.perf_counter()
196
+ y, oof = leave_one_out(rows, method)
197
+ run_time = time.perf_counter() - start
198
+ summary = score(y, oof)
199
+
200
+ # the shipped head sees all n subjects, so it is not any of the models scored above
201
+ method.fit(rows)
202
+ method.save(run_dir / "model")
203
+
204
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
205
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
206
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
207
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
208
+
209
+
210
+ def predict(args: argparse.Namespace) -> None:
211
+ """The challenge contract: modality paths in, one probability written to `--output`.
212
+
213
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
214
+ code leave-one-out already ran, not something generated at build time.
215
+ """
216
+ overrides = {"device": args.device}
217
+ if args.ckpt_path:
218
+ overrides["ckpt_path"] = args.ckpt_path
219
+ method = Task1Method.load(args.model_dir, **overrides)
220
+
221
+ # every image the challenge hands over, as in `leave_one_out`; the method takes what it uses
222
+ paths = {"adc": args.adc, "dwi_b1000": args.dwi, "flair": args.flair}
223
+ probability = method.predict({key: nib.load(path) for key, path in paths.items()})
224
+
225
+ args.output.write_text(f"{probability:.6f}\n")
226
+
227
+
228
+ def main() -> None:
229
+ parser = argparse.ArgumentParser()
230
+ modes = parser.add_subparsers(required=True)
231
+
232
+ train_parser = modes.add_parser("train", help="leave-one-out over the task, then fit and save")
233
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
234
+ train_parser.set_defaults(run=train)
235
+
236
+ predict_parser = modes.add_parser("predict", help="one subject, one probability")
237
+ for flag in ("--flair", "--adc", "--dwi"):
238
+ predict_parser.add_argument(flag, type=Path, required=True)
239
+ # accepted and ignored: the 4th modality is swi on some subjects and t2s on others
240
+ for flag in ("--t2s", "--swi"):
241
+ predict_parser.add_argument(flag, type=Path)
242
+ predict_parser.add_argument("--output", type=Path, required=True)
243
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
244
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
245
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
246
+ predict_parser.set_defaults(run=predict)
247
+
248
+ args = parser.parse_args()
249
+ args.run(args)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task3.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 3: brain age regression, scored by Pearson r and MAE as the challenge scores it.
2
+
3
+ `Task3Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: 20-fold over the 494 subjects, pool the
5
+ out-of-fold predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract, one t1
8
+ path in and one age out. Both go through `Task3Method.predict`, so every fold exercises the path
9
+ the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import RidgeCV
25
+ from sklearn.model_selection import KFold
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task3"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ output_root: str = "output/fomo_tune"
44
+ name: str = "task3"
45
+ device: str = "cuda"
46
+ seed: int = 4466
47
+
48
+
49
+ # ---- method: the part we tune -----------------------------------------------------------
50
+
51
+
52
+ class Task3Method:
53
+ """Frozen sMRI MAE, mean-pooled tokens over the t1w, ridge head."""
54
+
55
+ def __init__(self, cfg: Config):
56
+ self.cfg = cfg
57
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
58
+ self.device = torch.device(cfg.device)
59
+ self.backbone.to(self.device).eval().requires_grad_(False)
60
+ self.cache: dict[str, np.ndarray] = {}
61
+ self.head = None
62
+
63
+ @torch.inference_mode()
64
+ def features(self, images: Images) -> np.ndarray:
65
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
66
+ sample = self.transform(images["t1w"])
67
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
68
+
69
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
70
+ out = self.backbone(batch)
71
+
72
+ patch_embeds = out["patch_embeds"]
73
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
74
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
75
+ return embed[0].float().cpu().numpy()
76
+
77
+ def cached_features(self, row: dict) -> np.ndarray:
78
+ if row["subject"] not in self.cache:
79
+ self.cache[row["subject"]] = self.features(row)
80
+ return self.cache[row["subject"]]
81
+
82
+ def fit(self, rows: list[dict]) -> None:
83
+ X = np.stack([self.cached_features(row) for row in rows])
84
+ y = np.array([row["age"] for row in rows], dtype=float)
85
+
86
+ # RidgeCV picks alpha by its own efficient leave-one-out, so the fold's own split is
87
+ # never touched by model selection
88
+ self.head = make_pipeline(StandardScaler(), RidgeCV(alphas=np.logspace(-3, 6, 19)))
89
+ self.head.fit(X, y)
90
+
91
+ def predict(self, images: Images) -> float:
92
+ """Age in years."""
93
+ X = self.features(images)[None]
94
+ return float(self.head.predict(X)[0])
95
+
96
+ def save(self, model_dir: Path) -> None:
97
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
98
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
99
+ model_dir.mkdir(parents=True, exist_ok=True)
100
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
101
+ joblib.dump(self.head, model_dir / "head.joblib")
102
+
103
+ @classmethod
104
+ def load(cls, model_dir: Path, **overrides) -> "Task3Method":
105
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
106
+ between here and the container -- the backbone path, the device."""
107
+ cfg = OmegaConf.merge(
108
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
109
+ )
110
+ method = cls(cfg)
111
+ method.head = joblib.load(model_dir / "head.joblib")
112
+ return method
113
+
114
+
115
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
116
+
117
+ # Every image the task ships. The method picks which of them it wants, as at inference, where the
118
+ # challenge hands over the modalities whether or not a model uses them.
119
+ IMAGE_COLS = ("t1w",)
120
+
121
+
122
+ def cross_validate(
123
+ rows: list[dict], method: Task3Method, seed: int = 0, n_folds: int = 20
124
+ ) -> tuple[np.ndarray, np.ndarray]:
125
+ """Out-of-fold age for every subject, each predicted by a head fit on the other folds."""
126
+ y = np.array([row["age"] for row in rows], dtype=float)
127
+ oof = np.zeros(len(rows), dtype=float)
128
+ folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
129
+ start = time.perf_counter()
130
+ for fold, (train, test) in enumerate(folds.split(rows)):
131
+ method.fit([rows[i] for i in train])
132
+ for i in test:
133
+ oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
134
+ logger.info(
135
+ f"fold {fold + 1}/{n_folds} n={len(test)} mae={np.abs(y[test] - oof[test]).mean():.2f} "
136
+ f"({time.perf_counter() - start:.0f}s)"
137
+ )
138
+ return y, oof
139
+
140
+
141
+ def metrics(y: np.ndarray, oof: np.ndarray) -> dict:
142
+ return {
143
+ "pearson_r": float(np.corrcoef(y, oof)[0, 1]),
144
+ "mae": float(np.abs(y - oof).mean()),
145
+ }
146
+
147
+
148
+ def score(
149
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
150
+ ) -> dict:
151
+ """Both challenge metrics, each with a percentile CI resampling subjects with replacement."""
152
+ rng = np.random.default_rng(seed)
153
+ resamples = rng.integers(0, len(y), size=(n_boot, len(y)))
154
+
155
+ summary = {}
156
+ for name, point in metrics(y, oof).items():
157
+ samples = [metrics(y[rows], oof[rows])[name] for rows in resamples]
158
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
159
+ summary[name] = point
160
+ summary[f"{name}_ci_low"] = float(low)
161
+ summary[f"{name}_ci_high"] = float(high)
162
+ return summary
163
+
164
+
165
+ # ---- entrypoints ------------------------------------------------------------------------
166
+
167
+
168
+ def train(args: argparse.Namespace) -> None:
169
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
170
+ from fomo_tune.datasets import load_fomo_task3
171
+
172
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
173
+ run_dir = Path(cfg.output_root) / cfg.name
174
+ run_dir.mkdir(parents=True, exist_ok=True)
175
+
176
+ setup_logging(run_dir)
177
+ set_seed(cfg.seed)
178
+ logger.info(f"run {cfg.name} (git {git_sha()})")
179
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
180
+ OmegaConf.save(cfg, run_dir / "config.yaml")
181
+
182
+ rows = list(load_fomo_task3())
183
+ ages = np.array([row["age"] for row in rows])
184
+ logger.info(
185
+ f"dataset: {len(rows)} subjects, age {ages.min()}-{ages.max()} mean {ages.mean():.1f}"
186
+ )
187
+
188
+ method = Task3Method(cfg)
189
+ start = time.perf_counter()
190
+ y, oof = cross_validate(rows, method)
191
+ run_time = time.perf_counter() - start
192
+ summary = score(y, oof)
193
+
194
+ # the shipped head sees all n subjects, so it is not any of the models scored above
195
+ method.fit(rows)
196
+ method.save(run_dir / "model")
197
+
198
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
199
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
200
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
201
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
202
+
203
+
204
+ def predict(args: argparse.Namespace) -> None:
205
+ """The challenge contract: a t1 path in, one age written to `--output`.
206
+
207
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
208
+ code cross-validation already ran, not something generated at build time.
209
+ """
210
+ overrides = {"device": args.device}
211
+ if args.ckpt_path:
212
+ overrides["ckpt_path"] = args.ckpt_path
213
+ method = Task3Method.load(args.model_dir, **overrides)
214
+
215
+ age = method.predict({"t1w": nib.load(args.t1)})
216
+
217
+ args.output.write_text(f"{age:.6f}\n")
218
+
219
+
220
+ def main() -> None:
221
+ parser = argparse.ArgumentParser()
222
+ modes = parser.add_subparsers(required=True)
223
+
224
+ train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
225
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
226
+ train_parser.set_defaults(run=train)
227
+
228
+ predict_parser = modes.add_parser("predict", help="one subject, one age in years")
229
+ predict_parser.add_argument("--t1", type=Path, required=True)
230
+ predict_parser.add_argument("--output", type=Path, required=True)
231
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
232
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
233
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
234
+ predict_parser.set_defaults(run=predict)
235
+
236
+ args = parser.parse_args()
237
+ args.run(args)
238
+
239
+
240
+ if __name__ == "__main__":
241
+ main()
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task5.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 5: polymicrogyria classification, scored by AUROC as the challenge scores it.
2
+
3
+ `Task5Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: 20-fold over the 48 subjects, pool the
5
+ out-of-fold predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract, one t1
8
+ path in and one probability out. Both go through `Task5Method.predict`, so every fold exercises
9
+ the path the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import LogisticRegressionCV
25
+ from sklearn.metrics import roc_auc_score
26
+ from sklearn.model_selection import KFold
27
+ from sklearn.pipeline import make_pipeline
28
+ from sklearn.preprocessing import StandardScaler
29
+
30
+ from fomo_tune.backbone import load_backbone
31
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
32
+
33
+ logger = logging.getLogger("fomo_tune")
34
+
35
+ Images = dict[str, nib.Nifti1Image]
36
+
37
+
38
+ @dataclass
39
+ class Config:
40
+ task: str = "task5"
41
+ ckpt_path: str = (
42
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
43
+ )
44
+ output_root: str = "output/fomo_tune"
45
+ name: str = "task5"
46
+ device: str = "cuda"
47
+ seed: int = 4466
48
+
49
+
50
+ # ---- method: the part we tune -----------------------------------------------------------
51
+
52
+
53
+ class Task5Method:
54
+ """Frozen sMRI MAE, mean-pooled tokens over the t1w, logistic head."""
55
+
56
+ def __init__(self, cfg: Config):
57
+ self.cfg = cfg
58
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
59
+ self.device = torch.device(cfg.device)
60
+ self.backbone.to(self.device).eval().requires_grad_(False)
61
+ self.cache: dict[str, np.ndarray] = {}
62
+ self.head = None
63
+
64
+ @torch.inference_mode()
65
+ def features(self, images: Images) -> np.ndarray:
66
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
67
+ sample = self.transform(images["t1w"])
68
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
69
+
70
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
71
+ out = self.backbone(batch)
72
+
73
+ patch_embeds = out["patch_embeds"]
74
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
75
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
76
+ return embed[0].float().cpu().numpy()
77
+
78
+ def cached_features(self, row: dict) -> np.ndarray:
79
+ if row["subject"] not in self.cache:
80
+ self.cache[row["subject"]] = self.features(row)
81
+ return self.cache[row["subject"]]
82
+
83
+ def fit(self, rows: list[dict]) -> None:
84
+ X = np.stack([self.cached_features(row) for row in rows])
85
+ y = np.array([row["label"] for row in rows])
86
+
87
+ clf = LogisticRegressionCV(
88
+ Cs=10,
89
+ class_weight="balanced",
90
+ scoring="roc_auc",
91
+ max_iter=1000,
92
+ l1_ratios=(0,),
93
+ use_legacy_attributes=False,
94
+ )
95
+ self.head = make_pipeline(StandardScaler(), clf)
96
+ self.head.fit(X, y)
97
+ self.positive = list(self.head.classes_).index(1)
98
+
99
+ def predict(self, images: Images) -> float:
100
+ """Positive-class probability. Indexes `classes_` rather than assuming column 1, which
101
+ would silently score the wrong class if the label order differed."""
102
+ X = self.features(images)[None]
103
+ probs = self.head.predict_proba(X)[0]
104
+ return float(probs[self.positive])
105
+
106
+ def save(self, model_dir: Path) -> None:
107
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
108
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
109
+ model_dir.mkdir(parents=True, exist_ok=True)
110
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
111
+ joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
112
+
113
+ @classmethod
114
+ def load(cls, model_dir: Path, **overrides) -> "Task5Method":
115
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
116
+ between here and the container -- the backbone path, the device."""
117
+ cfg = OmegaConf.merge(
118
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
119
+ )
120
+ method = cls(cfg)
121
+ state = joblib.load(model_dir / "head.joblib")
122
+ method.head, method.positive = state["head"], state["positive"]
123
+ return method
124
+
125
+
126
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
127
+
128
+ # Every image the task ships. The method picks which of them it wants, as at inference, where the
129
+ # challenge hands over the modalities whether or not a model uses them.
130
+ IMAGE_COLS = ("t1w",)
131
+
132
+
133
+ def cross_validate(
134
+ rows: list[dict], method: Task5Method, seed: int = 0, n_folds: int = 20
135
+ ) -> tuple[np.ndarray, np.ndarray]:
136
+ """Out-of-fold score for every subject, each predicted by a head fit on the other folds."""
137
+ y = np.array([row["label"] for row in rows])
138
+ oof = np.zeros(len(rows), dtype=float)
139
+ folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
140
+ start = time.perf_counter()
141
+ for fold, (train, test) in enumerate(folds.split(rows)):
142
+ method.fit([rows[i] for i in train])
143
+ for i in test:
144
+ oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
145
+ logger.info(
146
+ f"fold {fold + 1}/{n_folds} n={len(test)} y={y[test]} "
147
+ f"p={np.round(oof[test], 3)} ({time.perf_counter() - start:.0f}s)"
148
+ )
149
+ return y, oof
150
+
151
+
152
+ def score(
153
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
154
+ ) -> dict:
155
+ """AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
156
+ rng = np.random.default_rng(seed)
157
+ samples = []
158
+ for _ in range(n_boot):
159
+ rows = rng.integers(0, len(y), size=len(y))
160
+ if len(np.unique(y[rows])) < 2:
161
+ continue
162
+ samples.append(roc_auc_score(y[rows], oof[rows]))
163
+
164
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
165
+ return {
166
+ "auroc": float(roc_auc_score(y, oof)),
167
+ "auroc_ci_low": float(low),
168
+ "auroc_ci_high": float(high),
169
+ }
170
+
171
+
172
+ # ---- entrypoints ------------------------------------------------------------------------
173
+
174
+
175
+ def train(args: argparse.Namespace) -> None:
176
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
177
+ from fomo_tune.datasets import load_fomo_task5
178
+
179
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
180
+ run_dir = Path(cfg.output_root) / cfg.name
181
+ run_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ setup_logging(run_dir)
184
+ set_seed(cfg.seed)
185
+ logger.info(f"run {cfg.name} (git {git_sha()})")
186
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
187
+ OmegaConf.save(cfg, run_dir / "config.yaml")
188
+
189
+ rows = list(load_fomo_task5())
190
+ logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
191
+
192
+ method = Task5Method(cfg)
193
+ start = time.perf_counter()
194
+ y, oof = cross_validate(rows, method)
195
+ run_time = time.perf_counter() - start
196
+ summary = score(y, oof)
197
+
198
+ # the shipped head sees all n subjects, so it is not any of the models scored above
199
+ method.fit(rows)
200
+ method.save(run_dir / "model")
201
+
202
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
203
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
204
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
205
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
206
+
207
+
208
+ def predict(args: argparse.Namespace) -> None:
209
+ """The challenge contract: a t1 path in, one probability written to `--output`.
210
+
211
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
212
+ code cross-validation already ran, not something generated at build time.
213
+ """
214
+ overrides = {"device": args.device}
215
+ if args.ckpt_path:
216
+ overrides["ckpt_path"] = args.ckpt_path
217
+ method = Task5Method.load(args.model_dir, **overrides)
218
+
219
+ probability = method.predict({"t1w": nib.load(args.t1)})
220
+
221
+ args.output.write_text(f"{probability:.6f}\n")
222
+
223
+
224
+ def main() -> None:
225
+ parser = argparse.ArgumentParser()
226
+ modes = parser.add_subparsers(required=True)
227
+
228
+ train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
229
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
230
+ train_parser.set_defaults(run=train)
231
+
232
+ predict_parser = modes.add_parser("predict", help="one subject, one probability")
233
+ predict_parser.add_argument("--t1", type=Path, required=True)
234
+ predict_parser.add_argument("--output", type=Path, required=True)
235
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
236
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
237
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
238
+ predict_parser.set_defaults(run=predict)
239
+
240
+ args = parser.parse_args()
241
+ args.run(args)
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/utils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import random
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+ logger = logging.getLogger("fomo_tune")
11
+
12
+
13
+ def set_seed(seed: int) -> None:
14
+ random.seed(seed)
15
+ np.random.seed(seed)
16
+ torch.manual_seed(seed)
17
+
18
+
19
+ def git_sha() -> str:
20
+ kwargs = dict(cwd=Path(__file__).parent, capture_output=True, text=True, check=True)
21
+ sha = subprocess.run(["git", "rev-parse", "--short", "HEAD"], **kwargs).stdout.strip()
22
+ dirty = subprocess.run(["git", "status", "--porcelain", "-uno"], **kwargs).stdout.strip()
23
+ return f"{sha}-dirty" if dirty else sha
24
+
25
+
26
+ def setup_logging(run_dir: Path) -> None:
27
+ handlers = [logging.StreamHandler(sys.stdout), logging.FileHandler(run_dir / "log.txt")]
28
+ logger.setLevel(logging.INFO)
29
+ logger.handlers.clear()
30
+ for handler in handlers:
31
+ handler.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
32
+ logger.addHandler(handler)
33
+ logger.propagate = False
finetune/fomo_tune_baseline/output/task5/build/model/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task5
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task5
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/output/task5/build/predict.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+
4
+ from fomo_tune.main_task5 import main
5
+
6
+ sys.argv = [
7
+ sys.argv[0],
8
+ "predict",
9
+ *sys.argv[1:],
10
+ "--model-dir",
11
+ "/app/model",
12
+ "--ckpt-path",
13
+ "/app/model/backbone.pth",
14
+ ]
15
+
16
+ main()
finetune/fomo_tune_baseline/output/task5/build/smri_mae/config/default_pretrain.yaml ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Name of the run. Used for output directory suffix and wandb.
2
+ name: pretrain
3
+
4
+ # Description of the run. Goes in wandb notes.
5
+ notes: null
6
+
7
+ # Root output directory.
8
+ # The run writes to checkpoints/<name> when name is set.
9
+ output_dir: checkpoints
10
+
11
+ # Standard 3D structural MRI volume size.
12
+ img_size: [208, 240, 208]
13
+ patch_size: 8
14
+
15
+ # Masking.
16
+ mask_ratio: 0.80
17
+ pred_mask_ratio: null
18
+ pad_to_multiple: 32
19
+
20
+ # Model.
21
+ model: mae_vit_large
22
+ model_kwargs:
23
+ # target normalization: null/none, global, slice, or patch.
24
+ target_norm: none
25
+
26
+ no_decode_pos: false
27
+ mask_drop_scale: false
28
+
29
+ class_token: true
30
+ reg_tokens: 0
31
+ no_embed_class: false
32
+
33
+ decoder_depth: 4
34
+ drop_path_rate: 0.0
35
+
36
+ # Datasets.
37
+ datasets:
38
+ fomo_train:
39
+ url: datasets/FOMO_with_dwi/shard.{000000..001620}.tar
40
+ samples_per_epoch: 243200
41
+ shuffle: true
42
+ buffer_size: 8000
43
+ drop_last: true
44
+
45
+ fomo_val:
46
+ url: datasets/FOMO_with_dwi/shard.{001621..001800}.tar
47
+ samples_per_epoch: 26880
48
+ shuffle: false
49
+ buffer_size: 1000
50
+ drop_last: true
51
+
52
+ train_dataset: fomo_train
53
+ eval_datasets:
54
+ - fomo_val
55
+
56
+ # Data loader.
57
+ num_workers: 4
58
+ prefetch_factor: 2
59
+ presend_cuda: true
60
+
61
+ # Optimization.
62
+ epochs: 100
63
+ batch_size: 64
64
+ accum_iter: 1
65
+
66
+ base_lr: 0.001
67
+ min_lr: 1e-6
68
+ warmup_epochs: 10
69
+ weight_decay: 0.05
70
+ betas: [0.9, 0.95]
71
+ clip_grad: 1.0
72
+
73
+ amp: true
74
+ amp_dtype: bfloat16
75
+
76
+ # Checkpointing.
77
+ ckpt: null
78
+ resume: false
79
+ auto_resume: true
80
+ start_epoch: 0
81
+ checkpoint_period: 10
82
+ max_checkpoints: 5
83
+
84
+ # Evaluation.
85
+ eval_period: 10
86
+
87
+ # Sync checkpoints to an R2 bucket using the AWS CLI. Set to a URL to enable.
88
+ r2_sync: null
89
+
90
+ device: cuda
91
+ distributed: false
92
+ seed: 7338
93
+ eval_seed: 7338
94
+ debug: false
95
+
96
+ wandb: false
97
+ wandb_entity: null
98
+ wandb_project: smri-fm
finetune/fomo_tune_baseline/output/task5/build/smri_mae/main_pretrain.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Sophont, Inc
2
+ # This source code is licensed under the Apache License, Version 2.0
3
+ #
4
+ # References:
5
+ # deit: https://github.com/facebookresearch/deit/blob/main/main.py
6
+ # capi: https://github.com/facebookresearch/capi/blob/main/train_capi.py
7
+
8
+ import argparse
9
+ import datetime
10
+ import json
11
+ import math
12
+ import random
13
+ import subprocess
14
+ import time
15
+ from contextlib import nullcontext
16
+ from functools import partial
17
+ from itertools import islice
18
+ from pathlib import Path
19
+ from typing import Iterable, Sequence
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ import wandb
24
+ import webdataset as wds
25
+ from omegaconf import DictConfig, OmegaConf
26
+ from PIL import Image
27
+
28
+ from matplotlib import pyplot as plt
29
+ from torch import Tensor
30
+
31
+ import data.mri_data as mri_data
32
+ import smri_mae.model_mae as models_mae
33
+ import smri_mae.utils as ut
34
+ import smri_mae.visualization as vis
35
+
36
+ DEFAULT_CONFIG = Path(__file__).parent / "config/default_pretrain.yaml"
37
+
38
+ MODELS_DICT = models_mae.__dict__
39
+
40
+
41
+ def main(args: DictConfig):
42
+ # setup
43
+ ut.init_distributed_mode(args)
44
+ global_rank = ut.get_rank()
45
+ is_master = global_rank == 0
46
+ world_size = ut.get_world_size()
47
+ device = torch.device(args.device)
48
+ ut.configure_flash_sdpa()
49
+ ut.random_seed(args.seed, rank=global_rank)
50
+
51
+ if args.name and not args.output_dir.endswith(args.name):
52
+ args.output_dir = f"{args.output_dir}/{args.name}"
53
+ output_dir = Path(args.output_dir)
54
+
55
+ if is_master:
56
+ output_dir.mkdir(parents=True, exist_ok=True)
57
+ out_cfg_path = output_dir / "config.yaml"
58
+ if out_cfg_path.exists():
59
+ prev_cfg = OmegaConf.load(out_cfg_path)
60
+ assert args == prev_cfg, "current config doesn't match previous config"
61
+ else:
62
+ OmegaConf.save(args, out_cfg_path)
63
+
64
+ if args.wandb:
65
+ wandb.init(
66
+ entity=args.wandb_entity,
67
+ project=args.wandb_project,
68
+ name=args.name,
69
+ notes=args.notes,
70
+ config=OmegaConf.to_container(args),
71
+ )
72
+
73
+ ut.setup_for_distributed(log_path=output_dir / "log.txt")
74
+
75
+ print("pretraining 3D ViTMAE")
76
+ print(f"start: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
77
+ print(f"cwd: {Path.cwd()}")
78
+ print(ut.get_sha())
79
+ print("config:", OmegaConf.to_yaml(args), sep="\n")
80
+
81
+ # data loaders
82
+ train_loader, eval_loaders = create_data_loaders(args)
83
+
84
+ # model
85
+ model = MODELS_DICT[args.model](
86
+ img_size=args.img_size,
87
+ in_chans=args.get("in_chans", 1),
88
+ patch_size=args.patch_size,
89
+ **(args.get("model_kwargs") or {}),
90
+ )
91
+ model.to(device)
92
+ print("model:", model, sep="\n")
93
+ num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
94
+ print(f"num params: {num_params / 1e6:.1f}M")
95
+
96
+ model_without_ddp = model
97
+ if args.distributed:
98
+ model = torch.nn.parallel.DistributedDataParallel(
99
+ model,
100
+ device_ids=[args.gpu],
101
+ gradient_as_bucket_view=True,
102
+ )
103
+ model_without_ddp = model.module
104
+
105
+ # optimizer
106
+ total_batch_size = args.batch_size * args.accum_iter * world_size
107
+ print(
108
+ f"total batch size: {total_batch_size} = "
109
+ f"{args.batch_size} bs per gpu x {args.accum_iter} accum x {world_size} gpus"
110
+ )
111
+
112
+ if not args.get("lr"):
113
+ args.lr = args.base_lr * total_batch_size / 256
114
+ print(f"lr: {args.lr:.2e} = {args.base_lr:.2e} x {total_batch_size} / 256")
115
+ else:
116
+ print(f"lr: {args.lr:.2e}")
117
+
118
+ param_groups = ut.get_param_groups(model)
119
+ ut.update_lr(param_groups, args.lr)
120
+ ut.update_wd(param_groups, args.weight_decay)
121
+ # cast or else it corrupts the checkpoint
122
+ betas = tuple(args.betas) if args.betas is not None else None
123
+ optimizer = torch.optim.AdamW(param_groups, betas=betas, fused=True)
124
+
125
+ epoch_num_batches = len(train_loader)
126
+ steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
127
+ total_steps = args.epochs * steps_per_epoch
128
+ warmup_steps = args.warmup_epochs * steps_per_epoch
129
+ lr_schedule = ut.WarmupThenCosine(
130
+ base_value=args.lr,
131
+ final_value=args.min_lr,
132
+ total_iters=total_steps,
133
+ warmup_iters=warmup_steps,
134
+ )
135
+ print(f"full schedule: epochs = {args.epochs} (steps = {total_steps})")
136
+ print(f"warmup: epochs = {args.warmup_epochs} (steps = {warmup_steps})")
137
+
138
+ # loss scaling not needed for bfloat16 (according to timm)
139
+ if args.amp and args.amp_dtype != "bfloat16":
140
+ loss_scaler = torch.GradScaler(device.type)
141
+ else:
142
+ loss_scaler = None
143
+
144
+ # load checkpoint/resume training
145
+ ut.load_model(args, model_without_ddp, optimizer, loss_scaler)
146
+
147
+ print(f"start training for {args.epochs} epochs")
148
+ start_time = time.monotonic()
149
+ for epoch in range(args.start_epoch, args.epochs):
150
+ train_stats = train_one_epoch(
151
+ args,
152
+ model,
153
+ train_loader,
154
+ optimizer,
155
+ loss_scaler,
156
+ lr_schedule,
157
+ epoch,
158
+ device,
159
+ )
160
+ eval_stats = {}
161
+ eval_plots = {}
162
+ eval_period = args.get("eval_period", 1)
163
+ if eval_period and (epoch % eval_period == 0 or epoch == args.epochs - 1):
164
+ for name, loader in eval_loaders.items():
165
+ stats, plots = evaluate(
166
+ args,
167
+ model,
168
+ loader,
169
+ epoch,
170
+ device,
171
+ eval_name=name,
172
+ )
173
+ eval_stats.update(stats)
174
+ eval_plots.update(plots)
175
+
176
+ merged_stats = {"epoch": epoch, **train_stats, **eval_stats}
177
+ if is_master:
178
+ with (output_dir / "log.json").open("a") as f:
179
+ print(json.dumps(merged_stats), file=f)
180
+
181
+ for plot_name, img in eval_plots.items():
182
+ plot_name = plot_name.replace("/", "__")
183
+ img.save(output_dir / f"{plot_name}__{epoch:05d}.png")
184
+
185
+ ut.save_model(args, epoch, model_without_ddp, optimizer, loss_scaler)
186
+ sync_checkpoints_to_r2(args, output_dir)
187
+
188
+ if args.distributed:
189
+ torch.distributed.destroy_process_group()
190
+
191
+ total_time = time.monotonic() - start_time
192
+ print(f"done! training time: {datetime.timedelta(seconds=int(total_time))}")
193
+
194
+
195
+ def create_data_loaders(args: DictConfig):
196
+ data_loaders = {}
197
+ dataset_names = [args.train_dataset] + args.eval_datasets
198
+
199
+ for dataset_name in dataset_names:
200
+ dataset_config = args.datasets[dataset_name].copy()
201
+ drop_last = dataset_config.pop("drop_last")
202
+ is_train = dataset_name == args.train_dataset
203
+
204
+ print(f"loading dataset: {dataset_name}\n\n{OmegaConf.to_yaml(dataset_config)}")
205
+ shuffle = dataset_config["shuffle"]
206
+ samples_per_epoch = dataset_config.pop("samples_per_epoch")
207
+ dataset = mri_data.make_sparse_wds_dataset(
208
+ dataset_config["url"],
209
+ shuffle=shuffle,
210
+ buffer_size=dataset_config["buffer_size"],
211
+ )
212
+ num_workers = int(args.num_workers)
213
+ loader_kwargs = {
214
+ "batch_size": args.batch_size,
215
+ "collate_fn": partial(mri_data.collate, include_meta=not is_train),
216
+ "shuffle": False,
217
+ "num_workers": num_workers,
218
+ "persistent_workers": num_workers > 0,
219
+ "pin_memory": True,
220
+ "drop_last": drop_last,
221
+ "prefetch_factor": args.prefetch_factor,
222
+ }
223
+ loader = wds.WebLoader(dataset, **loader_kwargs)
224
+ num_batches = samples_per_epoch // (ut.get_world_size() * args.batch_size)
225
+ loader = loader.with_epoch(num_batches)
226
+ loader = loader.with_length(num_batches, silent=True)
227
+
228
+ data_loaders[dataset_name] = loader
229
+
230
+ train_loader = data_loaders.pop(args.train_dataset)
231
+ return train_loader, data_loaders
232
+
233
+
234
+ def sync_checkpoints_to_r2(args: DictConfig, output_dir: Path) -> None:
235
+ r2_sync_url = args.get("r2_sync")
236
+ if not r2_sync_url or not ut.is_main_process():
237
+ return
238
+
239
+ cmd = ["aws", "s3", "sync", str(output_dir), str(r2_sync_url), "--profile", "r2"]
240
+ print(f"syncing checkpoints to R2: {output_dir} -> {r2_sync_url}")
241
+ subprocess.run(cmd, check=True)
242
+
243
+
244
+ def train_one_epoch(
245
+ args: DictConfig,
246
+ model: nn.Module,
247
+ data_loader: Iterable,
248
+ optimizer: torch.optim.Optimizer,
249
+ loss_scaler: torch.GradScaler | None,
250
+ lr_schedule: Sequence[float],
251
+ epoch: int,
252
+ device: torch.device,
253
+ ):
254
+ model.train()
255
+
256
+ metric_logger = ut.MetricLogger(delimiter=" ")
257
+ metric_logger.add_meter("lr", ut.SmoothedValue(window_size=1, fmt="{value:.6f}"))
258
+ metric_logger.add_meter("grad", ut.SmoothedValue())
259
+ header = f"Train: [{epoch}]"
260
+ log_wandb = args.wandb and ut.is_main_process()
261
+
262
+ epoch_num_batches = len(data_loader)
263
+ steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
264
+
265
+ print_freq = args.get("print_freq", 100) if not args.debug else 1
266
+ num_batches = epoch_num_batches if not args.debug else 10
267
+ amp_dtype = getattr(torch, args.amp_dtype)
268
+ use_cuda = device.type == "cuda"
269
+ if use_cuda and args.presend_cuda:
270
+ data_loader = ut.pre_send_to_cuda_wrapper(
271
+ data_loader, device, dtype_map={torch.float16: amp_dtype}
272
+ )
273
+
274
+ optimizer.zero_grad()
275
+
276
+ for batch_idx, batch in enumerate(
277
+ metric_logger.log_every(data_loader, print_freq, header, total_steps=num_batches)
278
+ ):
279
+ if use_cuda and not args.presend_cuda:
280
+ batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
281
+
282
+ batch_step = batch_idx + 1
283
+ log_step = batch_step % print_freq == 0 or batch_step == num_batches
284
+ update_in_epoch = batch_idx // args.accum_iter
285
+ group_size = min(args.accum_iter, num_batches - update_in_epoch * args.accum_iter)
286
+ need_update = batch_step % args.accum_iter == 0 or batch_step == num_batches
287
+ global_step = epoch * steps_per_epoch + update_in_epoch
288
+ lr = lr_schedule[global_step]
289
+ if need_update:
290
+ ut.update_lr(optimizer.param_groups, lr)
291
+
292
+ images, img_mask = mri_data.densify_sparse_image_batch(
293
+ batch["image_values"],
294
+ batch["img_mask"],
295
+ (int(args.get("in_chans", 1)), *args.img_size),
296
+ dtype=amp_dtype,
297
+ )
298
+
299
+ sync_context = model.no_sync() if args.distributed and not need_update else nullcontext()
300
+ with sync_context:
301
+ with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
302
+ loss = model(
303
+ images,
304
+ img_mask=img_mask,
305
+ mask_ratio=args.mask_ratio,
306
+ pred_mask_ratio=args.pred_mask_ratio,
307
+ pad_to_multiple=args.pad_to_multiple,
308
+ with_state=False,
309
+ )
310
+
311
+ loss_for_log = loss.detach()
312
+ torch._assert_async(torch.isfinite(loss_for_log), "non-finite loss")
313
+
314
+ grad_norm = ut.backward_step(
315
+ loss / group_size,
316
+ optimizer,
317
+ scaler=loss_scaler,
318
+ need_update=need_update,
319
+ max_norm=args.clip_grad,
320
+ )
321
+
322
+ if need_update and log_step:
323
+ loss_value = loss_for_log.item()
324
+ grad_norm_value = grad_norm.item()
325
+ metric_logger.update(loss=loss_value, lr=lr, grad=grad_norm_value)
326
+ if log_wandb:
327
+ wandb.log(
328
+ {
329
+ "train/loss": loss_value,
330
+ "train/lr": lr,
331
+ "train/grad": grad_norm_value,
332
+ },
333
+ step=int(1000 * (epoch + batch_step / epoch_num_batches)),
334
+ )
335
+
336
+ # gather the stats from all processes
337
+ metric_logger.synchronize_between_processes()
338
+ print("Averaged stats:", metric_logger)
339
+ return {f"train/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
340
+
341
+
342
+ @torch.inference_mode()
343
+ def evaluate(
344
+ args: DictConfig,
345
+ model: nn.Module,
346
+ data_loader: Iterable,
347
+ epoch: int,
348
+ device: torch.device,
349
+ eval_name: str,
350
+ ):
351
+ model.eval()
352
+
353
+ metric_logger = ut.MetricLogger(delimiter=" ")
354
+ header = f"Eval ({eval_name}): [{epoch}]"
355
+ is_master = ut.is_main_process()
356
+ log_wandb = args.wandb and is_master
357
+
358
+ epoch_num_batches = len(data_loader)
359
+ if epoch_num_batches <= 0:
360
+ raise ValueError(f"eval loader {eval_name!r} has zero batches")
361
+
362
+ print_freq = args.get("print_freq", 100) if not args.debug else 1
363
+ num_batches = epoch_num_batches if not args.debug else 10
364
+ num_batches = min(num_batches, epoch_num_batches)
365
+ eval_seed = int(args.get("eval_seed", args.seed)) + ut.get_rank()
366
+ example_step = random.Random(eval_seed).randint(1, num_batches)
367
+ amp_dtype = getattr(torch, args.amp_dtype)
368
+ use_cuda = device.type == "cuda"
369
+ rng_state = ut.capture_rng_state()
370
+ torch.set_rng_state(torch.Generator().manual_seed(eval_seed).get_state())
371
+ if use_cuda:
372
+ torch.cuda.manual_seed(eval_seed)
373
+ if use_cuda and args.presend_cuda:
374
+ data_loader = ut.pre_send_to_cuda_wrapper(
375
+ data_loader, device, dtype_map={torch.float16: amp_dtype}
376
+ )
377
+
378
+ eval_batches = islice(data_loader, num_batches)
379
+ for batch_idx, batch in enumerate(
380
+ metric_logger.log_every(eval_batches, print_freq, header, total_steps=num_batches)
381
+ ):
382
+ if use_cuda and not args.presend_cuda:
383
+ batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
384
+
385
+ batch_step = batch_idx + 1
386
+
387
+ images, img_mask = mri_data.densify_sparse_image_batch(
388
+ batch["image_values"],
389
+ batch["img_mask"],
390
+ (int(args.get("in_chans", 1)), *args.img_size),
391
+ dtype=amp_dtype,
392
+ )
393
+
394
+ with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
395
+ loss, state = model(
396
+ images,
397
+ img_mask=img_mask,
398
+ mask_ratio=args.mask_ratio,
399
+ pred_mask_ratio=args.pred_mask_ratio,
400
+ pad_to_multiple=args.pad_to_multiple,
401
+ )
402
+
403
+ loss_value = loss.detach().float().item()
404
+ finite = torch.tensor(int(math.isfinite(loss_value)), dtype=torch.int32, device=device)
405
+ if args.distributed:
406
+ torch.distributed.all_reduce(finite, op=torch.distributed.ReduceOp.MIN)
407
+ if not finite.item():
408
+ raise RuntimeError("non-finite validation loss detected")
409
+ metric_logger.meters["loss"].update(loss_value, n=int(batch["img_mask"].shape[0]))
410
+
411
+ if is_master and batch_step == example_step:
412
+ example_batch = {"image": images[:1], "img_mask": img_mask[:1]}
413
+ if "meta" in batch:
414
+ example_batch["meta"] = batch["meta"][:1]
415
+ example_state = {
416
+ "pred_images": state["pred_images"][:1],
417
+ "pred_mask": state["pred_mask"][:1],
418
+ }
419
+ example_data = {
420
+ "batch": ut.send_data(example_batch, "cpu"),
421
+ "state": ut.send_data(example_state, "cpu"),
422
+ }
423
+
424
+ # gather the stats from all processes
425
+ metric_logger.synchronize_between_processes()
426
+ print(f"Averaged stats ({eval_name}):", metric_logger)
427
+ stats = {f"eval/{eval_name}/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
428
+
429
+ plots = {}
430
+ if is_master:
431
+ print(f"Making plots ({eval_name}): example={example_step}")
432
+ plots = make_plots(args, **example_data)
433
+ plots = {f"eval/{eval_name}/{k}": img for k, img in plots.items()}
434
+
435
+ if log_wandb:
436
+ wandb.log(stats, step=1000 * (epoch + 1))
437
+ wandb.log(
438
+ {k: wandb.Image(img, caption=f"example={example_step}") for k, img in plots.items()},
439
+ step=1000 * (epoch + 1),
440
+ )
441
+ ut.restore_rng_state(rng_state)
442
+ return stats, plots
443
+
444
+
445
+ def make_plots(
446
+ args: DictConfig,
447
+ batch: dict[str, Tensor],
448
+ state: dict[str, Tensor],
449
+ ) -> dict[str, Image.Image]:
450
+ fig_kwargs = args.get("fig_kwargs", {})
451
+
452
+ images = batch["image"]
453
+ img_mask = batch.get("img_mask")
454
+ if img_mask is not None:
455
+ img_mask = img_mask.expand_as(images)
456
+
457
+ raw_mean, raw_std = vis.raw_stats_from_batch(batch)
458
+
459
+ plots = {}
460
+ mask_pred_fig = vis.plot_mask_pred(
461
+ target=images,
462
+ pred=state["pred_images"],
463
+ pred_mask=state["pred_mask"],
464
+ img_mask=img_mask,
465
+ patch_size=args.patch_size,
466
+ raw_mean=raw_mean,
467
+ raw_std=raw_std,
468
+ **ut.filter_kwargs(vis.plot_mask_pred, fig_kwargs),
469
+ )
470
+ plots["mask_pred"] = vis.fig2pil(mask_pred_fig)
471
+ plt.close(mask_pred_fig)
472
+
473
+ return plots
474
+
475
+
476
+ if __name__ == "__main__":
477
+ parser = argparse.ArgumentParser()
478
+ parser.add_argument("--cfg-path", type=str, default=None)
479
+ parser.add_argument("--overrides", type=str, default=None, nargs="+")
480
+ args = parser.parse_args()
481
+ cfg = OmegaConf.load(DEFAULT_CONFIG)
482
+ if args.cfg_path:
483
+ cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.load(args.cfg_path))
484
+ if args.overrides:
485
+ cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.from_dotlist(args.overrides))
486
+ main(cfg)
finetune/fomo_tune_baseline/output/task5/build/smri_mae/masking.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from jaxtyping import Float, Int
3
+ from torch import Tensor
4
+
5
+
6
+ def pad_patch_mask(
7
+ patch_mask: Float[Tensor, "B N"],
8
+ mask_ratio: float,
9
+ shuffle: bool = False,
10
+ generator: torch.Generator | None = None,
11
+ pad_to_multiple: int | None = None,
12
+ ) -> tuple[Float[Tensor, "B N"], Int[Tensor, "B L"], Tensor]:
13
+ """
14
+ Select each row's own mask-ratio count, then pad ids to the batch max length.
15
+
16
+ Returns:
17
+ - selected patch mask [B, N]
18
+ - padded selected patch ids [B, Lpad]
19
+ - token mask [B, Lpad], true for real ids and false for padding
20
+ """
21
+ if not 0.0 <= mask_ratio <= 1.0:
22
+ raise ValueError(f"mask_ratio must be in [0, 1], got {mask_ratio}")
23
+
24
+ B, N = patch_mask.shape
25
+ device = patch_mask.device
26
+ patch_mask = patch_mask.to(dtype=torch.bool)
27
+
28
+ valid_counts = patch_mask.sum(dim=1)
29
+ num_keep = torch.floor(valid_counts.to(torch.float64) * (1.0 - mask_ratio)).to(torch.long)
30
+ if not shuffle:
31
+ selected = patch_mask & (patch_mask.cumsum(dim=1) <= num_keep.unsqueeze(1))
32
+ padded_ids, token_mask = patch_ids_from_mask(
33
+ selected,
34
+ pad_to_multiple=pad_to_multiple,
35
+ )
36
+ return selected, padded_ids, token_mask
37
+
38
+ # One masked sort directly produces random valid IDs. The previous
39
+ # shuffle/select/inverse-shuffle path required two full argsorts plus a
40
+ # dynamic nonzero/scatter solely to recover the same selected set.
41
+ noise = torch.rand(B, N, generator=generator, device=device)
42
+ noise.masked_fill_(~patch_mask, torch.inf)
43
+ shuffled_ids = torch.argsort(noise, dim=1)
44
+
45
+ max_count = int(num_keep.max().item())
46
+ if pad_to_multiple is not None:
47
+ if pad_to_multiple <= 0:
48
+ raise ValueError(f"pad_to_multiple must be positive, got {pad_to_multiple}")
49
+ max_count = (max_count + pad_to_multiple - 1) // pad_to_multiple * pad_to_multiple
50
+ padded_ids = shuffled_ids[:, :max_count]
51
+ token_mask = torch.arange(max_count, device=device).unsqueeze(0) < num_keep.unsqueeze(1)
52
+ selected = torch.zeros_like(patch_mask).scatter_(1, padded_ids, token_mask)
53
+ return selected, padded_ids, token_mask
54
+
55
+
56
+ def patch_ids_from_mask(
57
+ patch_mask: Tensor,
58
+ pad_to_multiple: int | None = None,
59
+ ) -> tuple[Int[Tensor, "B L"], Tensor]:
60
+ """Return optionally rounded patch IDs and their token-validity mask."""
61
+ if pad_to_multiple is not None and pad_to_multiple <= 0:
62
+ raise ValueError(f"pad_to_multiple must be positive, got {pad_to_multiple}")
63
+
64
+ patch_mask = patch_mask.to(dtype=torch.bool)
65
+ B, N = patch_mask.shape
66
+ device = patch_mask.device
67
+ counts = patch_mask.sum(dim=1)
68
+ max_count = int(counts.max().item())
69
+ if pad_to_multiple is not None:
70
+ max_count = (max_count + pad_to_multiple - 1) // pad_to_multiple * pad_to_multiple
71
+
72
+ patch_ids = torch.zeros((B, max_count), dtype=torch.long, device=device)
73
+ token_mask = torch.arange(max_count, device=device).unsqueeze(0) < counts.unsqueeze(1)
74
+ if max_count == 0:
75
+ return patch_ids, token_mask
76
+
77
+ batch_ids, selected_ids = patch_mask.nonzero(as_tuple=True)
78
+ slot_ids = patch_mask.cumsum(dim=1)[batch_ids, selected_ids].to(torch.long) - 1
79
+ patch_ids[batch_ids, slot_ids] = selected_ids
80
+ return patch_ids, token_mask
finetune/fomo_tune_baseline/output/task5/build/smri_mae/model_mae.py ADDED
@@ -0,0 +1,916 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Sophont, Inc
2
+ # This source code is licensed under the Apache License, Version 2.0
3
+ #
4
+ # References:
5
+ # capi: https://github.com/facebookresearch/capi/blob/main/model.py
6
+ # timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
7
+
8
+ """
9
+ From-scratch re-implementation of the original MAE model.
10
+
11
+ MaskedEncoder: standard ViT with masking
12
+ MaskedDecoder: standard self-attention MAE decoder
13
+ MaskedAutoEncoderViT: full MAE model for 3D structural MRI volumes
14
+ """
15
+
16
+ from collections.abc import Sequence
17
+ from typing import Literal
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from torch import Tensor
23
+ from torch.utils.checkpoint import checkpoint
24
+ from huggingface_hub import PyTorchModelHubMixin
25
+ from jaxtyping import Float, Int
26
+
27
+ from .modules import (
28
+ AbsolutePosEmbed,
29
+ Block,
30
+ LayerNorm,
31
+ Normalize,
32
+ JaggedBatch,
33
+ Patchify3D,
34
+ SeparablePosEmbed,
35
+ SinCosPosEmbed3D,
36
+ unpack_tokens,
37
+ )
38
+ from .masking import pad_patch_mask
39
+
40
+
41
+ class MaskedEncoder(nn.Module):
42
+ """
43
+ Masked transformer encoder.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ patchify: nn.Module,
49
+ patch_embed: nn.Module,
50
+ pos_embed: nn.Module,
51
+ depth: int = 12,
52
+ embed_dim: int = 768,
53
+ num_heads: int = 12,
54
+ qkv_bias: bool = True,
55
+ proj_bias: bool = True,
56
+ mlp_ratio: int | float = 4,
57
+ class_token: bool = True,
58
+ reg_tokens: int = 0,
59
+ no_embed_class: bool = False,
60
+ final_norm: bool = True,
61
+ drop_path_rate: float = 0.0,
62
+ mask_drop_scale: bool = False,
63
+ ):
64
+ super().__init__()
65
+ self.num_prefix_tokens = int(class_token) + reg_tokens
66
+ self.num_reg_tokens = reg_tokens
67
+ self.has_class_token = class_token
68
+ self.no_embed_class = no_embed_class
69
+
70
+ # scale inputs by 1 / observed rate (like dropout)
71
+ self.mask_drop_scale = mask_drop_scale
72
+
73
+ # inject tokenization modules, so that the encoder doesn't specifically need to
74
+ # know how the data are tokenized, while still implementing a complete
75
+ # self-contained model.
76
+ self.patchify = patchify
77
+ self.patch_embed = patch_embed
78
+ self.pos_embed = pos_embed
79
+
80
+ R = reg_tokens
81
+ self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
82
+ self.reg_token = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
83
+
84
+ if not no_embed_class:
85
+ self.cls_token_pos = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
86
+ self.reg_token_pos = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
87
+ else:
88
+ self.cls_token_pos = self.reg_token_pos = None
89
+
90
+ # stochastic depth decay rule
91
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
92
+
93
+ self.blocks = nn.ModuleList(
94
+ [
95
+ Block(
96
+ dim=embed_dim,
97
+ num_heads=num_heads,
98
+ qkv_bias=qkv_bias,
99
+ proj_bias=proj_bias,
100
+ mlp_ratio=mlp_ratio,
101
+ drop_path=dpr[ii],
102
+ )
103
+ for ii in range(depth)
104
+ ]
105
+ )
106
+
107
+ self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
108
+
109
+ self.reset_parameters()
110
+
111
+ def extra_repr(self):
112
+ return (
113
+ f"class_token={self.has_class_token}, reg_tokens={self.num_reg_tokens}, "
114
+ f"no_embed_class={self.no_embed_class}, mask_drop_scale={self.mask_drop_scale}"
115
+ )
116
+
117
+ def reset_parameters(self) -> None:
118
+ for p in [self.cls_token, self.cls_token_pos, self.reg_token, self.reg_token_pos]:
119
+ if p is not None:
120
+ nn.init.trunc_normal_(p, std=0.02)
121
+
122
+ def cat_tokens(self, x: Tensor) -> Tensor:
123
+ # prepend cls and reg tokens with optional learned position embedding
124
+ # the cls and reg pos embedding is ofc redundant, but included in many other
125
+ # implementations.
126
+ B, _, _ = x.shape
127
+
128
+ to_cat = []
129
+ if self.has_class_token:
130
+ cls_token = self.cls_token
131
+ if not self.no_embed_class:
132
+ cls_token = cls_token + self.cls_token_pos
133
+ to_cat.append(cls_token.expand(B, -1, -1))
134
+
135
+ if self.num_reg_tokens:
136
+ reg_token = self.reg_token
137
+ if not self.no_embed_class:
138
+ reg_token = reg_token + self.reg_token_pos
139
+ to_cat.append(reg_token.expand(B, -1, -1))
140
+
141
+ if to_cat:
142
+ x = torch.cat(to_cat + [x], dim=1)
143
+ return x
144
+
145
+ def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
146
+ if self.num_prefix_tokens:
147
+ prefix_mask = torch.ones(
148
+ (batch_size, self.num_prefix_tokens),
149
+ dtype=torch.bool,
150
+ device=token_mask.device,
151
+ )
152
+ token_mask = torch.cat([prefix_mask, token_mask], dim=1)
153
+ return token_mask
154
+
155
+ def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor | None, Tensor]:
156
+ cls_offset = int(self.has_class_token)
157
+ cls = x[:, :cls_offset] if self.has_class_token else None
158
+ if self.num_reg_tokens:
159
+ reg = x[:, cls_offset : self.num_prefix_tokens, :]
160
+ else:
161
+ reg = None
162
+ patch = x[:, self.num_prefix_tokens :, :]
163
+ return cls, reg, patch
164
+
165
+ def forward(
166
+ self,
167
+ x: Tensor,
168
+ mask: Tensor | None = None,
169
+ mask_ratio: float | None = None,
170
+ pad_to_multiple: int | None = None,
171
+ ) -> tuple[
172
+ Float[Tensor, "B 1 D"] | None,
173
+ Float[Tensor, "B R D"] | None,
174
+ Float[Tensor, "B L D"],
175
+ Tensor | None,
176
+ Int[Tensor, "B L"] | None,
177
+ Tensor | None,
178
+ ]:
179
+ """
180
+ x: input data shape [B, C, D, H, W]
181
+ mask: visible mask, 1 = visible, 0 = invisible. broadcastable shape
182
+ mask_ratio: mask ratio for uniform random masking
183
+
184
+ returns:
185
+ - cls_embeds: [B, 1, D]
186
+ - reg_embeds: [B, R, D]
187
+ - patch_embeds: [B, L, D], where L is the number of visible patches
188
+ - mask: observed mask, 1 = observed, 0 = unobserved. same shape as input
189
+ - mask_ids: indices of visible patches [B L]
190
+ - token_mask: valid token mask for padded per-sample masking [B L]
191
+ """
192
+ # apply mask to the input
193
+ if mask is not None:
194
+ mask = mask.to(device=x.device, dtype=torch.bool).expand_as(x)
195
+ x = x.masked_fill(~mask, 0)
196
+
197
+ # patchify input
198
+ x = self.patchify(x)
199
+ B, N, P = x.shape
200
+
201
+ # patchify mask and apply dropout style scaling
202
+ if mask is not None:
203
+ mask_patches = self.patchify(mask)
204
+ patch_num_obs = mask_patches.sum(dim=-1)
205
+ patch_mask = patch_num_obs > 0
206
+ if self.mask_drop_scale:
207
+ patch_num_obs = patch_num_obs.to(x.dtype)
208
+ x = x * (P / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
209
+ elif mask_ratio is not None:
210
+ patch_mask = torch.ones((B, N), dtype=torch.bool, device=x.device)
211
+ mask_patches = patch_mask.unsqueeze(-1).expand(-1, -1, P)
212
+ else:
213
+ patch_mask = mask_patches = None
214
+
215
+ # patch and position embed
216
+ x = self.patch_embed(x)
217
+ x = self.pos_embed(x)
218
+
219
+ if mask is not None or mask_ratio is not None:
220
+ mask_ratio = 0.0 if mask_ratio is None else mask_ratio
221
+ patch_mask, mask_ids, token_mask = pad_patch_mask(
222
+ patch_mask,
223
+ mask_ratio=mask_ratio,
224
+ shuffle=mask_ratio > 0,
225
+ pad_to_multiple=pad_to_multiple,
226
+ )
227
+
228
+ mask_patches = mask_patches & patch_mask.unsqueeze(-1)
229
+ mask = self.patchify.unpatchify(mask_patches)
230
+ x = x.gather(1, mask_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
231
+ else:
232
+ mask_ids = None
233
+ token_mask = None
234
+
235
+ cls_embeds, reg_embeds, patch_embeds = self.forward_patch_embeds(
236
+ x,
237
+ token_mask=token_mask,
238
+ )
239
+ return cls_embeds, reg_embeds, patch_embeds, mask, mask_ids, token_mask
240
+
241
+ def forward_patch_embeds(
242
+ self,
243
+ x: Float[Tensor, "B L D"],
244
+ token_mask: Tensor | None = None,
245
+ ) -> tuple[
246
+ Float[Tensor, "B 1 D"] | None,
247
+ Float[Tensor, "B R D"] | None,
248
+ Float[Tensor, "B L D"],
249
+ ]:
250
+ B = x.shape[0]
251
+ if token_mask is None:
252
+ token_mask = torch.ones(x.shape[:2], dtype=torch.bool, device=x.device)
253
+ x = self.cat_tokens(x)
254
+ token_mask = self.cat_token_mask(token_mask, B)
255
+ jagged_batch = JaggedBatch.from_mask(token_mask)
256
+ x = x[token_mask]
257
+ for block in self.blocks:
258
+ x = block(x, jagged_batch=jagged_batch)
259
+ x = self.norm(x)
260
+ x = unpack_tokens(x, token_mask)
261
+
262
+ cls_embeds, reg_embeds, patch_embeds = self.chunk_tokens(x)
263
+ return cls_embeds, reg_embeds, patch_embeds
264
+
265
+ def forward_visible_ids(
266
+ self,
267
+ x: Tensor,
268
+ visible_ids: Int[Tensor, "B L"],
269
+ img_mask: Tensor | None = None,
270
+ ) -> tuple[
271
+ Float[Tensor, "B 1 D"] | None,
272
+ Float[Tensor, "B R D"] | None,
273
+ Float[Tensor, "B L D"],
274
+ ]:
275
+ if img_mask is not None:
276
+ img_mask = img_mask.to(device=x.device, dtype=torch.bool).expand_as(x)
277
+ x = x.masked_fill(~img_mask, 0)
278
+
279
+ x = self.patchify(x)
280
+ if self.mask_drop_scale and img_mask is not None:
281
+ mask_patches = self.patchify(img_mask)
282
+ patch_num_obs = mask_patches.sum(dim=-1).to(x.dtype)
283
+ x = x * (self.patchify.patch_dim / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
284
+ x = self.patch_embed(x)
285
+ x = self.pos_embed(x)
286
+ visible_ids = visible_ids.to(device=x.device)
287
+ x = x.gather(1, visible_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
288
+ return self.forward_patch_embeds(x)
289
+
290
+ def forward_embedding(
291
+ self,
292
+ x: Tensor,
293
+ mask: Tensor | None = None,
294
+ mask_ratio: float | None = None,
295
+ ):
296
+ cls_embeds, reg_embeds, patch_embeds, *_ = self.forward(
297
+ x,
298
+ mask=mask,
299
+ mask_ratio=mask_ratio,
300
+ )
301
+ return cls_embeds, reg_embeds, patch_embeds
302
+
303
+
304
+ class MaskedDecoder(nn.Module):
305
+ """Self-attention MAE decoder supporting sparse subset decoding via pred_ids."""
306
+
307
+ def __init__(
308
+ self,
309
+ pos_embed: nn.Module,
310
+ head: nn.Module | None = None,
311
+ input_dim: int | None = None,
312
+ depth: int = 12,
313
+ embed_dim: int = 768,
314
+ num_heads: int = 12,
315
+ qkv_bias: bool = True,
316
+ proj_bias: bool = True,
317
+ mlp_ratio: int | float = 4,
318
+ class_token: bool = True,
319
+ no_embed_class: bool = False,
320
+ final_norm: bool = True,
321
+ ):
322
+ super().__init__()
323
+ input_dim = embed_dim if input_dim is None else input_dim
324
+ self.has_class_token = class_token
325
+ self.no_embed_class = no_embed_class
326
+
327
+ self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
328
+ self.cls_token_pos = (
329
+ nn.Parameter(torch.empty(1, 1, embed_dim))
330
+ if class_token and not no_embed_class
331
+ else None
332
+ )
333
+ self.mask_token = nn.Parameter(torch.empty(1, 1, embed_dim))
334
+
335
+ # decoder position embedding, encodes query position information into masks
336
+ self.pos_embed = pos_embed
337
+
338
+ self.proj = nn.Identity() if input_dim == embed_dim else nn.Linear(input_dim, embed_dim)
339
+
340
+ self.blocks = nn.ModuleList(
341
+ [
342
+ Block(
343
+ dim=embed_dim,
344
+ num_heads=num_heads,
345
+ qkv_bias=qkv_bias,
346
+ proj_bias=proj_bias,
347
+ mlp_ratio=mlp_ratio,
348
+ )
349
+ for _ in range(depth)
350
+ ]
351
+ )
352
+
353
+ self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
354
+
355
+ # optional injected prediction head
356
+ self.head = nn.Identity() if head is None else head
357
+
358
+ self.reset_parameters()
359
+
360
+ def extra_repr(self):
361
+ return f"class_token={self.has_class_token}, no_embed_class={self.no_embed_class}"
362
+
363
+ def reset_parameters(self) -> None:
364
+ # official mae initializes decoder cls token to zeros
365
+ # although perhaps this was an oversight
366
+ if self.cls_token is not None:
367
+ nn.init.zeros_(self.cls_token)
368
+ if self.cls_token_pos is not None:
369
+ nn.init.trunc_normal_(self.cls_token_pos, std=0.02)
370
+ nn.init.trunc_normal_(self.mask_token, std=0.02)
371
+
372
+ def cat_tokens(self, x: Tensor) -> Tensor:
373
+ if not self.has_class_token:
374
+ return x
375
+ cls_token = self.cls_token
376
+ if not self.no_embed_class:
377
+ cls_token = cls_token + self.cls_token_pos
378
+ return torch.cat([cls_token.expand(x.shape[0], -1, -1), x], dim=1)
379
+
380
+ def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
381
+ if self.has_class_token:
382
+ cls_mask = torch.ones(
383
+ (batch_size, 1),
384
+ dtype=torch.bool,
385
+ device=token_mask.device,
386
+ )
387
+ token_mask = torch.cat([cls_mask, token_mask], dim=1)
388
+ return token_mask
389
+
390
+ def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor]:
391
+ cls_offset = int(self.has_class_token)
392
+ cls = x[:, :cls_offset] if self.has_class_token else None
393
+ patch = x[:, cls_offset:, :]
394
+ return cls, patch
395
+
396
+ def forward(
397
+ self,
398
+ embeds: Float[Tensor, "B L D"],
399
+ embed_ids: Int[Tensor, "B L"] | None = None,
400
+ pred_ids: Int[Tensor, "B Q"] | None = None,
401
+ embed_token_mask: Tensor | None = None,
402
+ pred_token_mask: Tensor | None = None,
403
+ packed_output: bool = False,
404
+ ) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
405
+ """
406
+ embeds: input patch embeddings.
407
+ embed_ids: optional patch indices for input embeddings. If not provided, no
408
+ position will be added to the embeddings.
409
+ pred_ids: patch indices of query mask positions. If None, decode *all* patches.
410
+
411
+ returns:
412
+ - pred [B, Q, P] where Q is the number of prediction patches and P is the output
413
+ dimension
414
+ """
415
+ B, L, _ = embeds.shape
416
+
417
+ Q = self.pos_embed.num_patches if pred_ids is None else pred_ids.shape[1]
418
+ mask = self.mask_token.expand(B, Q, -1)
419
+ mask = self.pos_embed(mask, pos_ids=pred_ids)
420
+
421
+ embeds = self.proj(embeds)
422
+
423
+ if embed_ids is not None:
424
+ embeds = self.pos_embed(embeds, pos_ids=embed_ids)
425
+ if embed_token_mask is None:
426
+ embed_token_mask = torch.ones((B, L), dtype=torch.bool, device=embeds.device)
427
+ if pred_token_mask is None:
428
+ pred_token_mask = torch.ones((B, Q), dtype=torch.bool, device=embeds.device)
429
+ x = torch.cat([embeds, mask], dim=1)
430
+ token_mask = torch.cat([embed_token_mask, pred_token_mask], dim=1)
431
+
432
+ x = self.cat_tokens(x)
433
+ token_mask = self.cat_token_mask(token_mask, B)
434
+ jagged_batch = JaggedBatch.from_mask(token_mask)
435
+ x = x[token_mask]
436
+ # Keep headroom for rare maximum-length PSP batches.
437
+ checkpoint_start = max(0, len(self.blocks) - 2)
438
+ for block_index, block in enumerate(self.blocks):
439
+ if self.training and torch.is_grad_enabled() and block_index >= checkpoint_start:
440
+ x = checkpoint(block, x, jagged_batch, use_reentrant=False)
441
+ else:
442
+ x = block(x, jagged_batch=jagged_batch)
443
+
444
+ x = self.norm(x)
445
+ if packed_output:
446
+ pred_offset = int(self.has_class_token) + L
447
+ prediction_mask = F.pad(pred_token_mask, (pred_offset, 0))
448
+ return self.head(x[prediction_mask[token_mask]])
449
+
450
+ x = unpack_tokens(x, token_mask)
451
+ _, x = self.chunk_tokens(x)
452
+
453
+ pred = x[:, L:]
454
+ pred = pred.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
455
+ pred = self.head(pred)
456
+ return pred
457
+
458
+
459
+ class MaskedAutoencoderViT(nn.Module, PyTorchModelHubMixin):
460
+ def __init__(
461
+ self,
462
+ img_size: int | tuple[int, int, int] = (208, 240, 208),
463
+ patch_size: int | tuple[int, int, int] = (16, 16, 16),
464
+ in_chans: int = 1,
465
+ depth: int = 12,
466
+ embed_dim: int = 768,
467
+ num_heads: int = 12,
468
+ decoder_depth: int = 4,
469
+ decoder_embed_dim: int | None = 512,
470
+ decoder_num_heads: int | None = 16, # default from mae, head dim = 32
471
+ qkv_bias: bool = True,
472
+ proj_bias: bool = True,
473
+ mlp_ratio: int | float = 4,
474
+ class_token: bool = True,
475
+ reg_tokens: int = 0,
476
+ no_embed_class: bool = False,
477
+ drop_path_rate: float = 0.0,
478
+ mask_drop_scale: bool = False,
479
+ no_decode_pos: bool = False,
480
+ pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
481
+ target_norm: Literal["none", "global", "slice", "patch"] | None = None,
482
+ ):
483
+ super().__init__()
484
+ img_size = _to_3d_tuple(img_size, "img_size")
485
+ patch_size = _to_3d_tuple(patch_size, "patch_size")
486
+
487
+ self.no_decode_pos = no_decode_pos # don't pos encode embeddings in decoder
488
+
489
+ # patchify reshapes input into sequence of flattened patches, shape [B, N, P]
490
+ ndim = 3
491
+ patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
492
+
493
+ # linear patch embedding P -> D
494
+ patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
495
+
496
+ # position embedding
497
+ # separable position embedding decouples the first spatial axis from the
498
+ # others. Fixed sin/cos embeddings are the default for sMRI volumes.
499
+ if pos_embed == "sincos":
500
+ pos_embed_layer = SinCosPosEmbed3D
501
+ else:
502
+ pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
503
+ pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
504
+
505
+ # encoder. for inference, this model can be extracted and used like a regular vit
506
+ self.encoder = MaskedEncoder(
507
+ patchify=patchify,
508
+ patch_embed=patch_embed,
509
+ pos_embed=pos_embed,
510
+ depth=depth,
511
+ embed_dim=embed_dim,
512
+ num_heads=num_heads,
513
+ qkv_bias=qkv_bias,
514
+ proj_bias=proj_bias,
515
+ mlp_ratio=mlp_ratio,
516
+ class_token=class_token,
517
+ reg_tokens=reg_tokens,
518
+ no_embed_class=no_embed_class,
519
+ drop_path_rate=drop_path_rate,
520
+ mask_drop_scale=mask_drop_scale,
521
+ )
522
+
523
+ self.pred_patchify = patchify
524
+
525
+ # fall back to encoder architecture width
526
+ decoder_embed_dim = decoder_embed_dim or embed_dim
527
+ decoder_num_heads = decoder_num_heads or num_heads
528
+
529
+ decoder_pos_embed = pos_embed_layer(decoder_embed_dim, self.pred_patchify.grid_size)
530
+ # we might want to try tying the weights of the prediction head to the patch
531
+ # embedding at some point.
532
+ decoder_head = nn.Linear(decoder_embed_dim, self.pred_patchify.patch_dim)
533
+
534
+ self.decoder = MaskedDecoder(
535
+ pos_embed=decoder_pos_embed,
536
+ head=decoder_head,
537
+ input_dim=embed_dim,
538
+ depth=decoder_depth,
539
+ embed_dim=decoder_embed_dim,
540
+ num_heads=decoder_num_heads,
541
+ qkv_bias=qkv_bias,
542
+ proj_bias=proj_bias,
543
+ mlp_ratio=mlp_ratio,
544
+ class_token=class_token,
545
+ no_embed_class=no_embed_class,
546
+ )
547
+
548
+ # mae style target normalization
549
+ # dim is relative to an unflattened embedding tensor of shape [B, *grid_size, D]
550
+ if target_norm not in {"none", None}:
551
+ norm_dim = {
552
+ "global": tuple(range(1, ndim + 2)), # full sequence
553
+ "slice": tuple(range(2, ndim + 2)), # each depth slice along first dim
554
+ "patch": -1, # normalize each patch independently (mae pix norm loss)
555
+ }[target_norm]
556
+ self.target_norm = Normalize(self.pred_patchify.grid_size, dim=norm_dim)
557
+ else:
558
+ self.target_norm = None
559
+
560
+ self.init_weights()
561
+
562
+ def extra_repr(self):
563
+ return f"no_decode_pos={self.no_decode_pos}"
564
+
565
+ def init_weights(self):
566
+ self.apply(_init_weights)
567
+
568
+ def prepare_targets(self, images: Tensor, img_mask: Tensor | None):
569
+ """
570
+ images: [B, C, D, H, W]
571
+ img_mask: mask of valid data. only used for computing correct normalization
572
+ stats. same shape as images.
573
+ """
574
+ targets_patches = self.pred_patchify(images) # [B, N, P]
575
+
576
+ # target normalization
577
+ if self.target_norm is not None:
578
+ # full image data mask used for normalization stats only
579
+ if img_mask is not None:
580
+ img_mask_patches = self.pred_patchify(img_mask)
581
+ else:
582
+ img_mask_patches = None
583
+ targets_patches, *targets_stats = self.target_norm(targets_patches, img_mask_patches)
584
+ else:
585
+ targets_stats = None
586
+
587
+ return targets_patches, targets_stats
588
+
589
+ def prepare_masks(
590
+ self,
591
+ img_mask: Tensor,
592
+ visible_mask: Tensor | None,
593
+ pred_mask: Tensor | None,
594
+ device: torch.device,
595
+ ):
596
+ img_mask = img_mask.to(device=device, dtype=torch.bool)
597
+
598
+ if visible_mask is None:
599
+ visible_mask = img_mask
600
+ else:
601
+ visible_mask = img_mask & visible_mask.to(device=device, dtype=torch.bool)
602
+
603
+ if pred_mask is None:
604
+ pred_mask = img_mask
605
+ else:
606
+ pred_mask = img_mask & pred_mask.to(device=device, dtype=torch.bool)
607
+
608
+ return img_mask, visible_mask, pred_mask
609
+
610
+ def prepare_pred_mask(
611
+ self,
612
+ visible_mask: Tensor,
613
+ pred_mask: Tensor | None = None,
614
+ pred_mask_ratio: float | None = None,
615
+ pad_to_multiple: int | None = None,
616
+ ):
617
+ """
618
+ prepare prediction mask by removing visible content
619
+ visible_mask: [B, C, D, H, W], 1 = visible, 0 = invisible
620
+ pred_mask: same shape, 1 = predict, 0 = don't predict
621
+ """
622
+ if pred_mask is None:
623
+ pred_mask = torch.ones_like(visible_mask)
624
+
625
+ pred_mask = pred_mask & ~visible_mask
626
+
627
+ pred_mask_patches = self.pred_patchify(pred_mask)
628
+ pred_patch_mask = pred_mask_patches.any(dim=-1)
629
+ # Optionally subsample the prediction candidates.
630
+ mask_ratio = 0.0 if pred_mask_ratio is None else pred_mask_ratio
631
+ pred_patch_mask, pred_ids, pred_token_mask = pad_patch_mask(
632
+ pred_patch_mask,
633
+ mask_ratio=mask_ratio,
634
+ # With per-sample padding every candidate is retained when the ratio
635
+ # is zero, so randomizing their order is pure overhead.
636
+ shuffle=mask_ratio > 0,
637
+ pad_to_multiple=pad_to_multiple,
638
+ )
639
+ pred_mask_patches = pred_mask_patches & pred_patch_mask.unsqueeze(-1)
640
+ return pred_mask_patches, pred_ids, pred_token_mask
641
+
642
+ def forward_decoder(
643
+ self,
644
+ patch_embeds: Float[Tensor, "B L D"],
645
+ visible_ids: Int[Tensor, "B L"],
646
+ pred_ids: Int[Tensor, "B Q"] | None,
647
+ visible_token_mask: Tensor | None = None,
648
+ pred_token_mask: Tensor | None = None,
649
+ packed_output: bool = False,
650
+ ) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
651
+ return self.decoder(
652
+ patch_embeds,
653
+ embed_ids=None if self.no_decode_pos else visible_ids,
654
+ pred_ids=pred_ids,
655
+ embed_token_mask=visible_token_mask,
656
+ pred_token_mask=pred_token_mask,
657
+ packed_output=packed_output,
658
+ )
659
+
660
+ def forward_loss(
661
+ self,
662
+ preds: Float[Tensor, "T P"],
663
+ targets_patches: Float[Tensor, "B N P"],
664
+ pred_mask_patches: Float[Tensor, "B N P"],
665
+ pred_ids: Int[Tensor, "B Q"],
666
+ pred_token_mask: Tensor,
667
+ ) -> Tensor:
668
+ """Average valid-voxel MSE within each scan, then average across scans."""
669
+ batch_ids, slot_ids = pred_token_mask.nonzero(as_tuple=True)
670
+ patch_ids = pred_ids[batch_ids, slot_ids]
671
+ targets = targets_patches[batch_ids, patch_ids]
672
+ voxel_mask = pred_mask_patches[batch_ids, patch_ids]
673
+
674
+ patch_errors = ((preds - targets) ** 2 * voxel_mask).sum(dim=1)
675
+ patch_voxels = voxel_mask.sum(dim=1).to(dtype=patch_errors.dtype)
676
+ batch_size = targets_patches.shape[0]
677
+ scan_errors = patch_errors.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_errors)
678
+ scan_voxels = patch_voxels.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_voxels)
679
+ return (scan_errors / scan_voxels).mean()
680
+
681
+ @torch.no_grad()
682
+ def forward_pred_images(
683
+ self,
684
+ preds: Float[Tensor, "B Q P"],
685
+ pred_ids: Int[Tensor, "B Q"],
686
+ pred_token_mask: Tensor | None = None,
687
+ img_mask: Tensor | None = None,
688
+ targets_stats: tuple[Tensor, Tensor] | None = None,
689
+ ) -> Tensor:
690
+ B, _, P = preds.shape
691
+ N = self.pred_patchify.num_patches
692
+ if pred_token_mask is not None:
693
+ preds = preds.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
694
+
695
+ preds = torch.zeros((B, N, P), dtype=preds.dtype, device=preds.device).scatter_add_(
696
+ 1, pred_ids.unsqueeze(-1).expand(-1, -1, P), preds
697
+ )
698
+
699
+ if targets_stats is not None:
700
+ targets_mean, targets_std = targets_stats
701
+ preds = preds * targets_std + targets_mean
702
+
703
+ pred_images = self.pred_patchify.unpatchify(preds)
704
+ if img_mask is not None:
705
+ pred_images = pred_images.masked_fill(~img_mask, 0)
706
+ return pred_images
707
+
708
+ def forward(
709
+ self,
710
+ images: Tensor,
711
+ img_mask: Tensor,
712
+ mask_ratio: float,
713
+ pred_mask_ratio: float | None = None,
714
+ pad_to_multiple: int | None = None,
715
+ with_state: bool = True,
716
+ ) -> Tensor | tuple[Tensor, dict]:
717
+ img_mask, visible_mask, pred_mask = self.prepare_masks(
718
+ img_mask,
719
+ None,
720
+ None,
721
+ device=images.device,
722
+ )
723
+ targets_patches, targets_stats = self.prepare_targets(images, img_mask)
724
+
725
+ (
726
+ cls_embeds,
727
+ reg_embeds,
728
+ patch_embeds,
729
+ visible_mask,
730
+ visible_ids,
731
+ visible_token_mask,
732
+ ) = self.encoder(
733
+ images,
734
+ mask=visible_mask,
735
+ mask_ratio=mask_ratio,
736
+ pad_to_multiple=pad_to_multiple,
737
+ )
738
+
739
+ pred_mask_patches, pred_ids, pred_token_mask = self.prepare_pred_mask(
740
+ visible_mask,
741
+ pred_mask=pred_mask,
742
+ pred_mask_ratio=pred_mask_ratio,
743
+ pad_to_multiple=pad_to_multiple,
744
+ )
745
+
746
+ preds = self.forward_decoder(
747
+ patch_embeds,
748
+ visible_ids,
749
+ pred_ids,
750
+ visible_token_mask=visible_token_mask,
751
+ pred_token_mask=pred_token_mask,
752
+ packed_output=not with_state,
753
+ )
754
+
755
+ loss_preds = preds if not with_state else preds[pred_token_mask]
756
+ loss = self.forward_loss(
757
+ loss_preds,
758
+ targets_patches,
759
+ pred_mask_patches,
760
+ pred_ids,
761
+ pred_token_mask,
762
+ )
763
+
764
+ if not with_state:
765
+ return loss
766
+
767
+ pred_mask = self.pred_patchify.unpatchify(pred_mask_patches)
768
+ pred_images = self.forward_pred_images(
769
+ preds,
770
+ pred_ids,
771
+ pred_token_mask=pred_token_mask,
772
+ img_mask=img_mask,
773
+ targets_stats=targets_stats,
774
+ )
775
+
776
+ state = {
777
+ "targets_patches": targets_patches,
778
+ "targets_stats": targets_stats,
779
+ "patch_embeds": patch_embeds,
780
+ "cls_embeds": cls_embeds,
781
+ "reg_embeds": reg_embeds,
782
+ "visible_mask": visible_mask,
783
+ "visible_ids": visible_ids,
784
+ "visible_token_mask": visible_token_mask,
785
+ "pred_mask": pred_mask,
786
+ "pred_ids": pred_ids,
787
+ "pred_token_mask": pred_token_mask,
788
+ "preds": preds,
789
+ "pred_images": pred_images,
790
+ }
791
+ return loss, state
792
+
793
+ def forward_embedding(
794
+ self,
795
+ x: Tensor,
796
+ mask: Tensor | None = None,
797
+ mask_ratio: float | None = None,
798
+ ):
799
+ return self.encoder.forward_embedding(x, mask=mask, mask_ratio=mask_ratio)
800
+
801
+
802
+ class MaskedViT(MaskedEncoder, PyTorchModelHubMixin):
803
+ def __init__(
804
+ self,
805
+ img_size: int | tuple[int, int, int] = (208, 240, 208),
806
+ in_chans: int = 1,
807
+ patch_size: int | tuple[int, int, int] = (16, 16, 16),
808
+ depth: int = 12,
809
+ embed_dim: int = 768,
810
+ num_heads: int = 12,
811
+ qkv_bias: bool = True,
812
+ proj_bias: bool = True,
813
+ mlp_ratio: int | float = 4,
814
+ class_token: bool = True,
815
+ reg_tokens: int = 0,
816
+ no_embed_class: bool = False,
817
+ final_norm: bool = True,
818
+ drop_path_rate: float = 0.0,
819
+ mask_drop_scale: bool = False,
820
+ pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
821
+ ):
822
+ img_size = _to_3d_tuple(img_size, "img_size")
823
+ patch_size = _to_3d_tuple(patch_size, "patch_size")
824
+
825
+ patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
826
+ patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
827
+ if pos_embed == "sincos":
828
+ pos_embed_layer = SinCosPosEmbed3D
829
+ else:
830
+ pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
831
+ pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
832
+
833
+ super().__init__(
834
+ patchify=patchify,
835
+ patch_embed=patch_embed,
836
+ pos_embed=pos_embed,
837
+ depth=depth,
838
+ embed_dim=embed_dim,
839
+ num_heads=num_heads,
840
+ qkv_bias=qkv_bias,
841
+ proj_bias=proj_bias,
842
+ mlp_ratio=mlp_ratio,
843
+ class_token=class_token,
844
+ reg_tokens=reg_tokens,
845
+ no_embed_class=no_embed_class,
846
+ final_norm=final_norm,
847
+ drop_path_rate=drop_path_rate,
848
+ mask_drop_scale=mask_drop_scale,
849
+ )
850
+
851
+ self.init_weights()
852
+
853
+ def init_weights(self):
854
+ self.apply(_init_weights)
855
+
856
+
857
+ def _to_3d_tuple(value: int | Sequence[int], name: str) -> tuple[int, int, int]:
858
+ if isinstance(value, int):
859
+ return (value, value, value)
860
+ if len(value) != 3:
861
+ raise ValueError(f"{name} must have exactly 3 spatial dimensions, got {tuple(value)}")
862
+ return tuple(int(item) for item in value)
863
+
864
+
865
+ # JAX ViT xavier uniform init
866
+ # https://github.com/facebookresearch/capi/blob/main/model.py
867
+ def _init_weights(m: nn.Module) -> None:
868
+ if isinstance(m, nn.Linear):
869
+ nn.init.xavier_uniform_(m.weight)
870
+ if m.bias is not None:
871
+ nn.init.constant_(m.bias, 0)
872
+ elif isinstance(m, nn.LayerNorm) and m.elementwise_affine:
873
+ nn.init.constant_(m.weight, 1.0)
874
+ if m.bias is not None:
875
+ nn.init.constant_(m.bias, 0)
876
+
877
+
878
+ def _create_vit(**kwargs):
879
+ model = MaskedViT(**kwargs)
880
+ return model
881
+
882
+
883
+ def _create_mae_vit(**kwargs):
884
+ model = MaskedAutoencoderViT(**kwargs)
885
+ return model
886
+
887
+
888
+ def mae_vit_small(**kwargs):
889
+ model_args = dict(embed_dim=384, depth=12, num_heads=6)
890
+ return _create_mae_vit(**model_args, **kwargs)
891
+
892
+
893
+ def mae_vit_base(**kwargs):
894
+ model_args = dict(embed_dim=768, depth=12, num_heads=12)
895
+ return _create_mae_vit(**model_args, **kwargs)
896
+
897
+
898
+ def mae_vit_large(**kwargs):
899
+ model_args = dict(embed_dim=1024, depth=24, num_heads=16)
900
+ return _create_mae_vit(**model_args, **kwargs)
901
+
902
+
903
+ def mae_vit_huge(**kwargs):
904
+ model_args = dict(embed_dim=1280, depth=32, num_heads=16)
905
+ return _create_mae_vit(**model_args, **kwargs)
906
+
907
+
908
+ # "patch embed" baseline model, depth 0 ViT (hah)
909
+ def patch_embed_small(**kwargs):
910
+ model_args = dict(embed_dim=384, depth=0)
911
+ return _create_vit(**model_args, **kwargs)
912
+
913
+
914
+ def patch_embed_base(**kwargs):
915
+ model_args = dict(embed_dim=768, depth=0)
916
+ return _create_vit(**model_args, **kwargs)
finetune/fomo_tune_baseline/output/task5/build/smri_mae/modules.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This source code is licensed under the Apache License, Version 2.0
2
+ #
3
+ # References:
4
+ # capi: https://github.com/facebookresearch/capi/blob/main/model.py
5
+ # timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
6
+ # vjepa2: https://github.com/facebookresearch/vjepa2/blob/main/src/models/utils/pos_embs.py
7
+
8
+ import math
9
+ from functools import partial
10
+ from typing import NamedTuple, Type
11
+
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+ from torch import Tensor
17
+ from einops import rearrange
18
+ from jaxtyping import Float, Int
19
+ from timm.layers import DropPath, to_3tuple
20
+
21
+ Layer = Type[nn.Module]
22
+
23
+
24
+ class JaggedBatch(NamedTuple):
25
+ """Sequence boundaries and cached launch metadata for jagged attention."""
26
+
27
+ offsets: Tensor
28
+ max_seqlen: int
29
+
30
+ @classmethod
31
+ def from_mask(cls, mask: Tensor) -> "JaggedBatch":
32
+ mask = mask.to(dtype=torch.bool)
33
+ counts = mask.sum(dim=1)
34
+ return cls(
35
+ offsets=F.pad(counts.cumsum(dim=0), (1, 0)),
36
+ max_seqlen=mask.shape[1],
37
+ )
38
+
39
+ def as_nested(self, tokens: Tensor) -> Tensor:
40
+ # Cached conservative bounds avoid min/max reductions and GPU-to-CPU
41
+ # synchronization when Flash SDPA inspects the jagged sequence lengths.
42
+ return torch.nested.nested_tensor_from_jagged(
43
+ tokens,
44
+ self.offsets,
45
+ min_seqlen=1,
46
+ max_seqlen=self.max_seqlen,
47
+ ).transpose(1, 2)
48
+
49
+
50
+ def unpack_tokens(tokens: Tensor, token_mask: Tensor) -> Tensor:
51
+ """Restore packed values to a padded batch, filling invalid slots with zero."""
52
+ output = tokens.new_zeros((*token_mask.shape, *tokens.shape[1:]))
53
+ return output.index_put((token_mask,), tokens)
54
+
55
+
56
+ def jagged_scaled_dot_product_attention(
57
+ query: Tensor,
58
+ key: Tensor,
59
+ value: Tensor,
60
+ jagged_batch: JaggedBatch,
61
+ ) -> Tensor:
62
+ """Run SDPA on a packed batch of variable-length sequences."""
63
+ output_jagged = F.scaled_dot_product_attention(
64
+ jagged_batch.as_nested(query),
65
+ jagged_batch.as_nested(key),
66
+ jagged_batch.as_nested(value),
67
+ )
68
+ return output_jagged.transpose(1, 2).values()
69
+
70
+
71
+ class Attention(nn.Module):
72
+ def __init__(
73
+ self,
74
+ dim: int,
75
+ num_heads: int,
76
+ qkv_bias: bool = False,
77
+ proj_bias: bool = False,
78
+ ) -> None:
79
+ super().__init__()
80
+ self.num_heads = num_heads
81
+ self.head_dim = dim // num_heads
82
+ self.qkv = nn.Linear(dim, 3 * dim, bias=qkv_bias)
83
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
84
+
85
+ def extra_repr(self):
86
+ return f"num_heads={self.num_heads}"
87
+
88
+ def forward(
89
+ self,
90
+ x: Float[Tensor, "L D"],
91
+ jagged_batch: JaggedBatch,
92
+ ) -> Float[Tensor, "L D"]:
93
+ L, D = x.shape
94
+ h, dh = self.num_heads, self.head_dim
95
+
96
+ qkv = self.qkv(x).reshape(L, 3, h, dh)
97
+ q, k, v = qkv.unbind(1)
98
+
99
+ x = jagged_scaled_dot_product_attention(
100
+ q,
101
+ k,
102
+ v,
103
+ jagged_batch=jagged_batch,
104
+ )
105
+ x = x.reshape(L, D)
106
+ x = self.proj(x)
107
+ return x
108
+
109
+
110
+ class Mlp(nn.Module):
111
+ def __init__(
112
+ self,
113
+ dim: int,
114
+ mlp_ratio: int | float = 4,
115
+ bias: bool = False,
116
+ ) -> None:
117
+ super().__init__()
118
+ hidden_features = int(dim * mlp_ratio)
119
+ self.fc1 = nn.Linear(dim, hidden_features, bias=bias)
120
+ self.act = nn.GELU()
121
+ self.fc2 = nn.Linear(hidden_features, dim, bias=bias)
122
+
123
+ def forward(self, x: Float[Tensor, "... D"]) -> Float[Tensor, "... D"]:
124
+ x = self.fc1(x)
125
+ x = self.act(x)
126
+ x = self.fc2(x)
127
+ return x
128
+
129
+
130
+ # timm default eps=1e-6
131
+ LayerNorm = partial(nn.LayerNorm, eps=1e-6)
132
+
133
+
134
+ class Block(nn.Module):
135
+ def __init__(
136
+ self,
137
+ dim: int,
138
+ num_heads: int,
139
+ qkv_bias: bool = False,
140
+ proj_bias: bool = False,
141
+ mlp_ratio: int | float = 4,
142
+ drop_path: float = 0.0,
143
+ norm_layer: Layer = LayerNorm,
144
+ ) -> None:
145
+ super().__init__()
146
+ self.norm1 = norm_layer(dim)
147
+ self.attn = Attention(
148
+ dim=dim,
149
+ num_heads=num_heads,
150
+ qkv_bias=qkv_bias,
151
+ proj_bias=proj_bias,
152
+ )
153
+ self.drop_path1 = DropPath(drop_path) if drop_path > 0 else nn.Identity()
154
+
155
+ self.norm2 = norm_layer(dim)
156
+ self.mlp = Mlp(
157
+ dim=dim,
158
+ mlp_ratio=mlp_ratio,
159
+ bias=proj_bias,
160
+ )
161
+ self.drop_path2 = DropPath(drop_path) if drop_path > 0 else nn.Identity()
162
+
163
+ def forward(
164
+ self,
165
+ x: Float[Tensor, "L D"],
166
+ jagged_batch: JaggedBatch,
167
+ ) -> Float[Tensor, "L D"]:
168
+ x = x + self.drop_path1(
169
+ self.attn(
170
+ self.norm1(x),
171
+ jagged_batch=jagged_batch,
172
+ )
173
+ )
174
+ x = x + self.drop_path2(self.mlp(self.norm2(x)))
175
+ return x
176
+
177
+
178
+ # Patching and position embedding modules
179
+
180
+
181
+ class Patchify3D(nn.Module):
182
+ def __init__(
183
+ self,
184
+ img_size: int | tuple[int, int, int],
185
+ patch_size: int | tuple[int, int, int],
186
+ in_chans: int = 3,
187
+ ) -> None:
188
+ super().__init__()
189
+ self.img_size = to_3tuple(img_size)
190
+ self.patch_size = to_3tuple(patch_size)
191
+ self.in_chans = in_chans
192
+
193
+ T, H, W = self.img_size
194
+ p_t, p_h, p_w = self.patch_size
195
+ if T % p_t or H % p_h or W % p_w:
196
+ raise ValueError(
197
+ f"img_size {self.img_size} must be divisible by patch_size {self.patch_size}"
198
+ )
199
+ self.grid_size = (T // p_t, H // p_h, W // p_w)
200
+ self.num_patches = math.prod(self.grid_size)
201
+ self.patch_dim = in_chans * math.prod(self.patch_size)
202
+
203
+ def forward(self, x: Float[Tensor, "B C T H W"]) -> Float[Tensor, "B N P"]:
204
+ x = patchify3d(x, self.patch_size)
205
+ return x
206
+
207
+ def unpatchify(self, x: Float[Tensor, "B N P"]) -> Float[Tensor, "B C T H W"]:
208
+ x = unpatchify3d(x, patch_size=self.patch_size, img_size=self.img_size)
209
+ return x
210
+
211
+ def extra_repr(self):
212
+ return f"{self.img_size}, {self.patch_size}, in_chans={self.in_chans}"
213
+
214
+
215
+ def patchify3d(x: Tensor, patch_size: tuple[int, int, int]) -> Tensor:
216
+ p_t, p_h, p_w = to_3tuple(patch_size)
217
+ B, C, T, H, W = x.shape
218
+ x = rearrange(x, "b c (t u) (h p) (w q) -> b (t h w) (c u p q)", u=p_t, p=p_h, q=p_w)
219
+ return x
220
+
221
+
222
+ def unpatchify3d(
223
+ x: Tensor,
224
+ patch_size: tuple[int, int, int],
225
+ img_size: tuple[int, int, int],
226
+ ) -> Tensor:
227
+ B, N, P = x.shape
228
+ p_t, p_h, p_w = to_3tuple(patch_size)
229
+ T, H, W = to_3tuple(img_size)
230
+ x = rearrange(
231
+ x,
232
+ "b (t h w) (c u p q) -> b c (t u) (h p) (w q)",
233
+ t=T // p_t,
234
+ h=H // p_h,
235
+ w=W // p_w,
236
+ u=p_t,
237
+ p=p_h,
238
+ q=p_w,
239
+ )
240
+ return x
241
+
242
+
243
+ class AbsolutePosEmbed(nn.Module):
244
+ def __init__(self, embed_dim: int, grid_size: tuple[int, ...]) -> None:
245
+ super().__init__()
246
+ self.embed_dim = embed_dim
247
+ self.grid_size = grid_size
248
+ self.num_patches = math.prod(grid_size)
249
+
250
+ self.weight = nn.Parameter(torch.empty(self.num_patches, embed_dim))
251
+ self.reset_parameters()
252
+
253
+ def reset_parameters(self):
254
+ nn.init.trunc_normal_(self.weight, std=0.02)
255
+
256
+ def forward(
257
+ self,
258
+ x: Float[Tensor, "B L D"],
259
+ pos_ids: Int[Tensor, "B L"] | None = None,
260
+ ) -> Float[Tensor, "B L D"]:
261
+ x = apply_pos_embed(x, self.weight, pos_ids=pos_ids)
262
+ return x
263
+
264
+ def extra_repr(self):
265
+ return f"{self.embed_dim}, {self.grid_size}"
266
+
267
+
268
+ class SeparablePosEmbed(nn.Module):
269
+ def __init__(self, embed_dim: int, grid_size: tuple[int, ...]) -> None:
270
+ super().__init__()
271
+ self.embed_dim = embed_dim
272
+ self.grid_size = grid_size
273
+ self.num_patches = math.prod(grid_size)
274
+
275
+ N_t, *grid_size_spatial = grid_size
276
+ N_s = math.prod(grid_size_spatial)
277
+ self.weight_spatial = nn.Parameter(torch.empty(1, N_s, embed_dim))
278
+ self.weight_temporal = nn.Parameter(torch.empty(N_t, 1, embed_dim))
279
+ self.reset_parameters()
280
+
281
+ def reset_parameters(self):
282
+ nn.init.trunc_normal_(self.weight_spatial, std=0.02)
283
+ nn.init.trunc_normal_(self.weight_temporal, std=0.02)
284
+
285
+ def forward(
286
+ self,
287
+ x: Float[Tensor, "B L D"],
288
+ pos_ids: Int[Tensor, "B L"] | None = None,
289
+ ) -> Float[Tensor, "B L D"]:
290
+ B, N, D = x.shape
291
+ weight = (self.weight_temporal + self.weight_spatial).flatten(0, 1) # [N, D]
292
+ x = apply_pos_embed(x, weight, pos_ids=pos_ids)
293
+ return x
294
+
295
+ def extra_repr(self):
296
+ return f"{self.embed_dim}, {self.grid_size}"
297
+
298
+
299
+ class SinCosPosEmbed3D(nn.Module):
300
+ def __init__(self, embed_dim: int, grid_size: tuple[int, int, int]) -> None:
301
+ super().__init__()
302
+ self.embed_dim = embed_dim
303
+ self.grid_size = grid_size
304
+ self.num_patches = math.prod(grid_size)
305
+
306
+ N_t, N_h, N_w = grid_size
307
+ weight = get_3d_sincos_pos_embed(
308
+ embed_dim=embed_dim,
309
+ grid_size=(N_h, N_w),
310
+ grid_depth=N_t,
311
+ uniform_power=True,
312
+ )
313
+ self.weight = nn.Parameter(torch.from_numpy(weight).float(), requires_grad=False)
314
+
315
+ def forward(
316
+ self,
317
+ x: Float[Tensor, "B L D"],
318
+ pos_ids: Int[Tensor, "B L"] | None = None,
319
+ ) -> Float[Tensor, "B L D"]:
320
+ x = apply_pos_embed(x, self.weight, pos_ids=pos_ids)
321
+ return x
322
+
323
+ def extra_repr(self):
324
+ return f"{self.embed_dim}, {self.grid_size}"
325
+
326
+
327
+ # sincos pos embed utils from vjepa2, but fixed the confusing meshgrid indexing
328
+
329
+
330
+ def get_3d_sincos_pos_embed(embed_dim, grid_size, grid_depth, cls_token=False, uniform_power=False):
331
+ """
332
+ grid_size: tuple of int of the grid height and width
333
+ grid_depth: int of the grid depth
334
+ returns:
335
+ pos_embed: [grid_depth*grid_height*grid_width, embed_dim] (w/o cls_token)
336
+ or [1+grid_depth*grid_height*grid_width, embed_dim] (w/ cls_token)
337
+ """
338
+ grid_d = np.arange(grid_depth, dtype=float)
339
+ grid_h = np.arange(grid_size[0], dtype=float)
340
+ grid_w = np.arange(grid_size[1], dtype=float)
341
+ grid_d, grid_h, grid_w = np.meshgrid(grid_d, grid_h, grid_w, indexing="ij")
342
+
343
+ if not uniform_power:
344
+ h_embed_dim = embed_dim // 4
345
+ w_embed_dim = embed_dim // 4
346
+ d_embed_dim = embed_dim // 2
347
+ else:
348
+ h_embed_dim = w_embed_dim = d_embed_dim = int(np.ceil(embed_dim / 6) * 2)
349
+
350
+ emb_h = get_1d_sincos_pos_embed_from_grid(h_embed_dim, grid_h) # (T*H*W, D1)
351
+ emb_w = get_1d_sincos_pos_embed_from_grid(w_embed_dim, grid_w) # (T*H*W, D2)
352
+ emb_d = get_1d_sincos_pos_embed_from_grid(d_embed_dim, grid_d) # (T*H*W, D3)
353
+ pos_embed = np.concatenate([emb_d, emb_h, emb_w], axis=1)
354
+ pos_embed = pos_embed[:, :embed_dim]
355
+ if cls_token:
356
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
357
+ return pos_embed
358
+
359
+
360
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
361
+ """
362
+ embed_dim: output dimension for each position
363
+ pos: a list of positions to be encoded: size (M,)
364
+ returns: (M, D)
365
+ """
366
+ assert embed_dim % 2 == 0
367
+ omega = np.arange(embed_dim // 2, dtype=float)
368
+ omega /= embed_dim / 2.0
369
+ omega = 1.0 / 10000**omega # (D/2,)
370
+
371
+ pos = pos.reshape(-1) # (M,)
372
+ out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
373
+
374
+ emb_sin = np.sin(out) # (M, D/2)
375
+ emb_cos = np.cos(out) # (M, D/2)
376
+
377
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
378
+ return emb
379
+
380
+
381
+ def apply_pos_embed(
382
+ x: Float[Tensor, "B L D"],
383
+ weight: Float[Tensor, "N D"],
384
+ pos_ids: Int[Tensor, "B L"] | None = None,
385
+ ) -> Float[Tensor, "B L D"]:
386
+ B, L, D = x.shape
387
+ weight = weight.expand(B, -1, -1)
388
+ if pos_ids is not None:
389
+ weight = weight.gather(1, pos_ids.unsqueeze(-1).expand(-1, -1, D))
390
+ x = x + weight
391
+ return x
392
+
393
+
394
+ # (masked) normalization used for MAE target normalization
395
+
396
+
397
+ class Normalize(nn.Module):
398
+ def __init__(
399
+ self,
400
+ grid_size: tuple[int, ...],
401
+ dim: int | tuple[int, ...] | None = -1,
402
+ eps: float = 1e-6,
403
+ ) -> None:
404
+ super().__init__()
405
+ self.grid_size = grid_size
406
+ self.dim = dim
407
+ self.eps = eps
408
+
409
+ def forward(self, x: Tensor, mask: Tensor | None = None) -> tuple[Tensor, Tensor, Tensor]:
410
+ """
411
+ Normalize input sequence along dim(s) after reshaping to grid.
412
+ Returns tuple of (x, mean, std).
413
+ """
414
+ B, N, D = x.shape
415
+ x = x.reshape((B, *self.grid_size, D))
416
+ if mask is not None:
417
+ mask = mask.reshape((B, *self.grid_size, D))
418
+ x, mean, std = masked_normalize(x, mask, dim=self.dim, eps=self.eps)
419
+ else:
420
+ x, mean, std = normalize(x, dim=self.dim, eps=self.eps)
421
+ mean = mean.expand_as(x).reshape(B, N, D)
422
+ std = std.expand_as(x).reshape(B, N, D)
423
+ x = x.reshape(B, N, D)
424
+ return x, mean, std
425
+
426
+ def extra_repr(self):
427
+ return f"{self.grid_size}, dim={self.dim}"
428
+
429
+
430
+ def masked_normalize(
431
+ x: Tensor,
432
+ mask: Tensor,
433
+ dim: int | tuple[int, ...] | None = -1,
434
+ eps: float = 1e-6,
435
+ ) -> tuple[Tensor, Tensor, Tensor]:
436
+ num_obs = mask.sum(dim=dim, keepdim=True).clamp(min=1)
437
+ mean = (mask * x).sum(dim=dim, keepdim=True) / num_obs
438
+ var = (mask * (x - mean) ** 2).sum(dim=dim, keepdim=True) / num_obs
439
+ std = (var + eps) ** 0.5
440
+ x = mask * (x - mean) / std
441
+ return x, mean, std
442
+
443
+
444
+ def normalize(
445
+ x: Tensor,
446
+ dim: int | tuple[int, ...] | None = -1,
447
+ eps: float = 1e-6,
448
+ ) -> tuple[Tensor, Tensor, Tensor]:
449
+ mean = x.mean(dim=dim, keepdim=True)
450
+ var = torch.var(x, dim=dim, keepdim=True, unbiased=False)
451
+ std = (var + eps) ** 0.5
452
+ x = (x - mean) / std
453
+ return x, mean, std
finetune/fomo_tune_baseline/output/task5/build/smri_mae/utils.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Sophont, Inc
2
+ # This source code is licensed under the Apache License, Version 2.0
3
+ #
4
+ # References:
5
+ # deit: https://github.com/facebookresearch/deit/blob/main/utils.py
6
+ # beit3: https://github.com/microsoft/unilm/blob/master/beit3/utils.py
7
+ # capi: https://github.com/facebookresearch/capi/blob/main/utils.py
8
+ # dinov2: https://github.com/facebookresearch/dinov2/blob/main/dinov2/utils/param_groups.py
9
+ # timm: https://github.com/huggingface/pytorch-image-models/blob/main/timm/utils/cuda.py
10
+ # dino: https://github.com/facebookresearch/dino/blob/main/utils.py
11
+
12
+ import datetime
13
+ import inspect
14
+ import math
15
+ import os
16
+ import random
17
+ import subprocess
18
+ import time
19
+ from collections import defaultdict, deque
20
+ from omegaconf import OmegaConf
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.distributed as dist
26
+ import torch.nn as nn
27
+ from torch import Tensor
28
+ from torch.amp import GradScaler
29
+ from torch.optim import Optimizer
30
+
31
+
32
+ # these very useful utils copied from deit with only minor changes
33
+ # thanks to the original authors, wherever you are
34
+
35
+
36
+ def configure_flash_sdpa() -> None:
37
+ """Use Flash Attention exclusively for CUDA SDPA."""
38
+ torch.backends.cuda.enable_flash_sdp(True)
39
+ torch.backends.cuda.enable_mem_efficient_sdp(False)
40
+ torch.backends.cuda.enable_math_sdp(False)
41
+ torch.backends.cuda.enable_cudnn_sdp(False)
42
+ print("SDPA backend: flash")
43
+
44
+
45
+ class SmoothedValue:
46
+ """Track a series of values and provide access to smoothed values over a
47
+ window or the global series average.
48
+ """
49
+
50
+ def __init__(self, window_size=20, fmt=None):
51
+ if fmt is None:
52
+ fmt = "{median:.4f} ({global_avg:.4f})"
53
+ self.deque = deque(maxlen=window_size)
54
+ self.total = 0.0
55
+ self.count = 0
56
+ self.fmt = fmt
57
+
58
+ def update(self, value, n=1):
59
+ value = float(value)
60
+ if math.isfinite(value):
61
+ self.deque.append(value)
62
+ self.count += n
63
+ self.total += value * n
64
+
65
+ def synchronize_between_processes(self):
66
+ """
67
+ Warning: does not synchronize the deque!
68
+ """
69
+ if not is_dist_avail_and_initialized():
70
+ return
71
+ t = torch.tensor([self.count, self.total], dtype=torch.float64, device="cuda")
72
+ dist.barrier()
73
+ dist.all_reduce(t)
74
+ t = t.tolist()
75
+ self.count = int(t[0])
76
+ self.total = t[1]
77
+
78
+ @property
79
+ def median(self):
80
+ if not self.count:
81
+ return float("nan")
82
+ d = torch.tensor(list(self.deque))
83
+ return d.median().item()
84
+
85
+ @property
86
+ def avg(self):
87
+ if not self.count:
88
+ return float("nan")
89
+ d = torch.tensor(list(self.deque), dtype=torch.float32)
90
+ return d.mean().item()
91
+
92
+ @property
93
+ def global_avg(self):
94
+ if not self.count:
95
+ return float("nan")
96
+ return self.total / self.count
97
+
98
+ @property
99
+ def max(self):
100
+ if not self.count:
101
+ return float("nan")
102
+ return max(self.deque)
103
+
104
+ @property
105
+ def value(self):
106
+ if not self.count:
107
+ return float("nan")
108
+ return self.deque[-1]
109
+
110
+ def __str__(self):
111
+ return self.fmt.format(
112
+ median=self.median,
113
+ avg=self.avg,
114
+ global_avg=self.global_avg,
115
+ max=self.max,
116
+ value=self.value,
117
+ )
118
+
119
+
120
+ class MetricLogger:
121
+ def __init__(self, delimiter="\t"):
122
+ self.meters = defaultdict(SmoothedValue)
123
+ self.delimiter = delimiter
124
+
125
+ def update(self, **kwargs):
126
+ for k, v in kwargs.items():
127
+ if v is None:
128
+ continue
129
+ if isinstance(v, (torch.Tensor, np.generic)):
130
+ v = v.item()
131
+ assert isinstance(v, (float, int))
132
+ self.meters[k].update(v)
133
+
134
+ def __getattr__(self, attr):
135
+ if attr in self.meters:
136
+ return self.meters[attr]
137
+ if attr in self.__dict__:
138
+ return self.__dict__[attr]
139
+ raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, attr))
140
+
141
+ def __str__(self):
142
+ loss_str = []
143
+ for name, meter in self.meters.items():
144
+ loss_str.append("{}: {}".format(name, str(meter)))
145
+ return self.delimiter.join(loss_str)
146
+
147
+ def synchronize_between_processes(self):
148
+ for meter in self.meters.values():
149
+ meter.synchronize_between_processes()
150
+
151
+ def add_meter(self, name, meter):
152
+ self.meters[name] = meter
153
+
154
+ def log_every(self, iterable, print_freq, header=None, total_steps=None):
155
+ i = 0
156
+ total_steps = total_steps or len(iterable)
157
+ if not header:
158
+ header = ""
159
+ start_time = time.time()
160
+ end = time.time()
161
+ iter_time = SmoothedValue(fmt="{avg:.4f}")
162
+ data_time = SmoothedValue(fmt="{avg:.4f}")
163
+ space_fmt = ":" + str(len(str(total_steps))) + "d"
164
+ log_msg = [
165
+ header,
166
+ "[{0" + space_fmt + "}/{1}]",
167
+ "eta: {eta}",
168
+ "{meters}",
169
+ "time: {time}",
170
+ "data: {data}",
171
+ ]
172
+ if torch.cuda.is_available():
173
+ log_msg.append("max mem: {memory:.0f}")
174
+ log_msg = self.delimiter.join(log_msg)
175
+ MB = 1024.0 * 1024.0
176
+ for obj in iterable:
177
+ if i >= total_steps:
178
+ break
179
+ data_time.update(time.time() - end)
180
+ yield obj
181
+ iter_time.update(time.time() - end)
182
+ if i % print_freq == 0 or i == total_steps - 1:
183
+ eta_seconds = iter_time.global_avg * (total_steps - i)
184
+ eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
185
+ if torch.cuda.is_available():
186
+ print(
187
+ log_msg.format(
188
+ i,
189
+ total_steps,
190
+ eta=eta_string,
191
+ meters=str(self),
192
+ time=str(iter_time),
193
+ data=str(data_time),
194
+ memory=torch.cuda.max_memory_allocated() / MB,
195
+ )
196
+ )
197
+
198
+ else:
199
+ print(
200
+ log_msg.format(
201
+ i,
202
+ total_steps,
203
+ eta=eta_string,
204
+ meters=str(self),
205
+ time=str(iter_time),
206
+ data=str(data_time),
207
+ )
208
+ )
209
+ i += 1
210
+ end = time.time()
211
+ total_time = time.time() - start_time
212
+ total_time_str = str(datetime.timedelta(seconds=int(total_time)))
213
+ print(
214
+ "{} Total time: {} ({:.4f} s / it)".format(
215
+ header, total_time_str, total_time / total_steps
216
+ )
217
+ )
218
+
219
+
220
+ def setup_for_distributed(log_path=None):
221
+ """
222
+ This function disables printing when not in master process
223
+ """
224
+ import builtins as __builtin__
225
+
226
+ builtin_print = __builtin__.print
227
+
228
+ is_master = is_main_process()
229
+
230
+ def print(*args, **kwargs):
231
+ force = kwargs.pop("force", False)
232
+ if is_master or force:
233
+ builtin_print(*args, **kwargs)
234
+ # tee to log file
235
+ if log_path and "file" not in kwargs:
236
+ with open(log_path, "a") as f:
237
+ builtin_print(*args, file=f, **kwargs)
238
+
239
+ __builtin__.print = print
240
+
241
+
242
+ def is_dist_avail_and_initialized():
243
+ if not dist.is_available():
244
+ return False
245
+ if not dist.is_initialized():
246
+ return False
247
+ return True
248
+
249
+
250
+ def get_world_size():
251
+ if not is_dist_avail_and_initialized():
252
+ return 1
253
+ return dist.get_world_size()
254
+
255
+
256
+ def get_rank():
257
+ if not is_dist_avail_and_initialized():
258
+ return 0
259
+ return dist.get_rank()
260
+
261
+
262
+ def is_main_process():
263
+ return get_rank() == 0
264
+
265
+
266
+ def save_on_master(obj, path, *args, **kwargs):
267
+ if is_main_process():
268
+ path = Path(path)
269
+ tmp_path = path.with_name(f".{path.name}.tmp-{os.getpid()}")
270
+ try:
271
+ torch.save(obj, tmp_path, *args, **kwargs)
272
+ os.replace(tmp_path, path)
273
+ except Exception:
274
+ tmp_path.unlink(missing_ok=True)
275
+ raise
276
+
277
+
278
+ def init_distributed_mode(args):
279
+ # removed slurm block, can add if we use slurm
280
+ if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
281
+ args.rank = int(os.environ["RANK"])
282
+ args.world_size = int(os.environ["WORLD_SIZE"])
283
+ args.gpu = int(os.environ["LOCAL_RANK"])
284
+ else:
285
+ args.distributed = False
286
+ return
287
+
288
+ args.distributed = True
289
+
290
+ torch.cuda.set_device(args.gpu)
291
+ args.dist_backend = "nccl"
292
+ print(f"| distributed init (rank {args.rank})")
293
+ torch.distributed.init_process_group(
294
+ backend=args.dist_backend,
295
+ world_size=args.world_size,
296
+ rank=args.rank,
297
+ device_id=args.gpu,
298
+ )
299
+ torch.distributed.barrier()
300
+
301
+
302
+ # checkpoint saving utils adapted from beit3
303
+
304
+
305
+ def capture_rng_state() -> dict:
306
+ state = {"torch": torch.get_rng_state()}
307
+ if torch.cuda.is_available():
308
+ state["cuda"] = torch.cuda.get_rng_state()
309
+ return state
310
+
311
+
312
+ def restore_rng_state(state: dict) -> None:
313
+ torch.set_rng_state(state["torch"])
314
+ if "cuda" in state and torch.cuda.is_available():
315
+ torch.cuda.set_rng_state(state["cuda"])
316
+
317
+
318
+ def _all_rank_rng_states() -> list[dict]:
319
+ local_state = capture_rng_state()
320
+ if not is_dist_avail_and_initialized():
321
+ return [local_state]
322
+ states = [None] * get_world_size()
323
+ dist.all_gather_object(states, local_state)
324
+ return states
325
+
326
+
327
+ def save_model(args, epoch, model_without_ddp, optimizer, loss_scaler):
328
+ output_dir = Path(args.output_dir)
329
+ checkpoint_path = output_dir / f"checkpoint-{epoch:05d}.pth"
330
+ last_checkpoint_path = output_dir / "checkpoint-last.pth"
331
+ if epoch % args.checkpoint_period != 0 and epoch != args.epochs - 1:
332
+ return
333
+
334
+ to_save = {
335
+ "model": model_without_ddp.state_dict(),
336
+ "optimizer": optimizer.state_dict(),
337
+ "epoch": epoch,
338
+ "scaler": None if loss_scaler is None else loss_scaler.state_dict(),
339
+ "args": OmegaConf.to_container(args),
340
+ "rng_states": _all_rank_rng_states(),
341
+ }
342
+
343
+ print(f"saving checkpoint {last_checkpoint_path}")
344
+ save_on_master(to_save, last_checkpoint_path)
345
+ print(f"saving checkpoint {checkpoint_path}")
346
+ save_on_master(to_save, checkpoint_path)
347
+
348
+ if args.max_checkpoints and is_main_process():
349
+ all_checkpoints = sorted(output_dir.glob("checkpoint-[0-9]*.pth"))
350
+ del_count = max(0, len(all_checkpoints) - args.max_checkpoints)
351
+ for checkpoint_path in all_checkpoints[:del_count]:
352
+ print(f"removing checkpoint {checkpoint_path}")
353
+ checkpoint_path.unlink()
354
+
355
+
356
+ def load_model(args, model_without_ddp, optimizer, loss_scaler):
357
+ auto_resume = getattr(args, "auto_resume", True)
358
+ output_dir = Path(args.output_dir)
359
+
360
+ last_checkpoint_path = output_dir / "checkpoint-last.pth"
361
+ if auto_resume and last_checkpoint_path.exists():
362
+ args.ckpt = str(last_checkpoint_path)
363
+ args.resume = True
364
+
365
+ if args.ckpt:
366
+ ckpt = torch.load(args.ckpt, map_location="cpu", weights_only=True)
367
+ model_without_ddp.load_state_dict(ckpt["model"])
368
+ print(f"loaded model state from checkpoint {args.ckpt}")
369
+
370
+ if args.resume:
371
+ optimizer.load_state_dict(ckpt["optimizer"])
372
+ if loss_scaler is not None:
373
+ loss_scaler.load_state_dict(ckpt["scaler"])
374
+ args.start_epoch = ckpt["epoch"] + 1
375
+ rng_states = ckpt.get("rng_states")
376
+ if rng_states is not None:
377
+ if len(rng_states) != get_world_size():
378
+ raise ValueError(
379
+ "checkpoint RNG state world size does not match current world size: "
380
+ f"{len(rng_states)} != {get_world_size()}"
381
+ )
382
+ restore_rng_state(rng_states[get_rank()])
383
+ print(f"restored RNG state for rank {get_rank()}")
384
+ print(f"loaded optimizer state, resuming training from {args.start_epoch}")
385
+
386
+
387
+ # optimization utils
388
+
389
+
390
+ # from capi
391
+ class WarmupThenCosine:
392
+ def __init__(
393
+ self,
394
+ base_value: float,
395
+ final_value: float,
396
+ total_iters: int,
397
+ warmup_iters: int = 0,
398
+ start_warmup_value: float = 0.0,
399
+ freeze_iters: int = 0,
400
+ truncate_cos: float = 1.0,
401
+ ):
402
+ super().__init__()
403
+ self.final_value = final_value
404
+ self.total_iters = total_iters
405
+
406
+ freeze_schedule = np.zeros(freeze_iters)
407
+
408
+ warmup_schedule = np.linspace(start_warmup_value, base_value, warmup_iters)
409
+
410
+ iters = np.arange(total_iters - warmup_iters - freeze_iters)
411
+ schedule = final_value + 0.5 * (base_value - final_value) * (
412
+ 1 + np.cos(np.pi * truncate_cos * iters / len(iters))
413
+ )
414
+ self.schedule = np.concatenate((freeze_schedule, warmup_schedule, schedule))
415
+ assert len(self.schedule) == self.total_iters
416
+
417
+ def __getitem__(self, it: int) -> float:
418
+ if it >= self.total_iters:
419
+ return self.final_value
420
+ # cast to float or else it can corrupt the checkpoint
421
+ return float(self.schedule[it])
422
+
423
+
424
+ # adapted from timm backward logic
425
+ # https://github.com/huggingface/pytorch-image-models/blob/main/timm/utils/cuda.py
426
+ def backward_step(
427
+ loss: Tensor,
428
+ optimizer: Optimizer,
429
+ scaler: GradScaler = None,
430
+ need_update: bool = True,
431
+ max_norm: float | None = None,
432
+ ) -> Tensor | None:
433
+ if scaler is not None:
434
+ scaler.scale(loss).backward()
435
+ else:
436
+ loss.backward()
437
+
438
+ if need_update:
439
+ if scaler is not None:
440
+ scaler.unscale_(optimizer)
441
+
442
+ total_norm = clip_grad(optimizer, max_norm)
443
+
444
+ if scaler is not None:
445
+ scaler.step(optimizer)
446
+ scaler.update()
447
+ else:
448
+ optimizer.step()
449
+ optimizer.zero_grad()
450
+ else:
451
+ total_norm = None
452
+ return total_norm
453
+
454
+
455
+ def clip_grad(optimizer: Optimizer, max_norm: float | None = None) -> Tensor:
456
+ params = [p for group in optimizer.param_groups for p in group["params"]]
457
+ if max_norm:
458
+ total_norm = nn.utils.clip_grad_norm_(params, max_norm, error_if_nonfinite=False)
459
+ else:
460
+ grads = [p.grad for p in params if p.grad is not None]
461
+ total_norm = nn.utils.get_total_norm(grads, error_if_nonfinite=False)
462
+ torch._assert_async(torch.isfinite(total_norm), "non-finite gradient norm")
463
+ return total_norm
464
+
465
+
466
+ # from dinov2 with some minor changes
467
+ def get_param_groups(model, patch_embed_lr_mult=1.0):
468
+ # no lr decay, we could add this later if needed
469
+ all_params = []
470
+
471
+ for name, param in model.named_parameters():
472
+ if not param.requires_grad:
473
+ continue
474
+ d = {"param": param, "lr_multiplier": 1.0, "wd_multiplier": 1.0, "name": name}
475
+
476
+ if name.endswith(".bias") or "norm" in name or "gamma" in name:
477
+ d["wd_multiplier"] = 0.0
478
+
479
+ if "patch_embed" in name:
480
+ d["lr_multiplier"] = d["lr_multiplier"] * patch_embed_lr_mult
481
+
482
+ all_params.append(d)
483
+
484
+ param_groups = _fuse_param_groups(all_params)
485
+ return param_groups
486
+
487
+
488
+ def _fuse_param_groups(all_param_groups):
489
+ fused_param_groups = defaultdict(lambda: {"params": []})
490
+ for d in all_param_groups:
491
+ keys = sorted(set(d.keys()) - {"param", "name"})
492
+ identifier = "_".join(f"{k}{d[k]}" for k in keys)
493
+ for k in keys:
494
+ fused_param_groups[identifier][k] = d[k]
495
+ fused_param_groups[identifier]["params"].append(d["param"])
496
+
497
+ param_groups = list(fused_param_groups.values())
498
+ return param_groups
499
+
500
+
501
+ def update_lr(param_groups, lr: float):
502
+ for group in param_groups:
503
+ group["lr"] = lr * group["lr_multiplier"]
504
+
505
+
506
+ def update_wd(param_groups, weight_decay: float | None = None):
507
+ for group in param_groups:
508
+ group["weight_decay"] = weight_decay * group["wd_multiplier"]
509
+
510
+
511
+ # moving data to cuda utils copied from capi
512
+ # added device argument
513
+
514
+
515
+ def send_data(x, device=None, dtype_map=None):
516
+ if device is None:
517
+ device = torch.device("cuda")
518
+ else:
519
+ device = torch.device(device)
520
+
521
+ if isinstance(x, torch.Tensor):
522
+ dtype = dtype_map.get(x.dtype) if dtype_map else None
523
+ return x.to(device=device, dtype=dtype, non_blocking=True)
524
+ if isinstance(x, dict):
525
+ return {k: send_data(v, device=device, dtype_map=dtype_map) for k, v in x.items()}
526
+ if isinstance(x, list):
527
+ return [send_data(v, device=device, dtype_map=dtype_map) for v in x]
528
+ return x
529
+
530
+
531
+ def pre_send_to_cuda_wrapper(generator, device=None, dtype_map=None):
532
+ """From apex"""
533
+ data = None
534
+ stream = torch.cuda.Stream(device)
535
+ for next_data in generator:
536
+ with torch.cuda.stream(stream):
537
+ next_data = send_data(next_data, device=device, dtype_map=dtype_map)
538
+ if data is not None:
539
+ yield data
540
+ torch.cuda.current_stream(device).wait_stream(stream)
541
+ data = next_data
542
+ if data is not None:
543
+ yield data
544
+
545
+
546
+ # other misc utils
547
+
548
+
549
+ # from dino
550
+ def get_sha():
551
+ cwd = os.path.dirname(os.path.abspath(__file__))
552
+
553
+ def _run(command):
554
+ return subprocess.check_output(command, cwd=cwd).decode("ascii").strip()
555
+
556
+ sha = "N/A"
557
+ diff = "clean"
558
+ branch = "N/A"
559
+ try:
560
+ sha = _run(["git", "rev-parse", "HEAD"])
561
+ diff = _run(["git", "diff-index", "HEAD"])
562
+ diff = "has uncommitted changes" if diff else "clean"
563
+ branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
564
+ except Exception:
565
+ pass
566
+ message = f"sha: {sha}, status: {diff}, branch: {branch}"
567
+ return message
568
+
569
+
570
+ # from timm
571
+ def random_seed(seed=42, rank=0):
572
+ torch.manual_seed(seed + rank)
573
+ np.random.seed(seed + rank)
574
+ random.seed(seed + rank)
575
+
576
+
577
+ # mine :)
578
+ def filter_kwargs(func, kwargs):
579
+ sigature = inspect.signature(func)
580
+ kwargs = {k: v for k, v in kwargs.items() if k in sigature.parameters}
581
+ return kwargs
finetune/fomo_tune_baseline/output/task5/build/smri_mae/visualization.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Mapping
2
+ from io import BytesIO
3
+
4
+ import torch
5
+
6
+ from matplotlib import patches
7
+ from matplotlib import pyplot as plt
8
+ from PIL import Image
9
+ from torch import Tensor
10
+
11
+ VIEW_NAMES = {
12
+ "sagittal": "Sagittal",
13
+ "saggital": "Sagittal",
14
+ "axial": "Axial",
15
+ "coronal": "Coronal",
16
+ }
17
+
18
+
19
+ def fig2pil(fig) -> Image.Image:
20
+ buffer = BytesIO()
21
+ fig.savefig(buffer, format="png", dpi=fig.dpi, facecolor=fig.get_facecolor())
22
+ buffer.seek(0)
23
+ image = Image.open(buffer).convert("RGB")
24
+ buffer.close()
25
+ return image
26
+
27
+
28
+ def raw_stats_from_batch(batch: dict) -> tuple[Tensor | None, Tensor | None]:
29
+ metas = batch.get("meta")
30
+ if not metas:
31
+ return None, None
32
+
33
+ means = []
34
+ stds = []
35
+ for meta in metas:
36
+ try:
37
+ mean = meta["raw_mean"]
38
+ std = meta["raw_std"]
39
+ except (KeyError, TypeError):
40
+ return None, None
41
+ if mean in ("", None) or std in ("", None):
42
+ return None, None
43
+ means.append(float(mean))
44
+ stds.append(float(std))
45
+ return torch.tensor(means), torch.tensor(stds)
46
+
47
+
48
+ def plot_mask_pred(
49
+ target: Tensor,
50
+ pred: Tensor,
51
+ pred_mask: Tensor | None = None,
52
+ img_mask: Tensor | None = None,
53
+ sample_idx: int = 0,
54
+ channel_idx: int = 0,
55
+ slice_idx: int | Mapping[str, int] | None = None,
56
+ patch_size: int | tuple[int, int, int] = 16,
57
+ views: tuple[str, ...] = ("sagittal", "axial", "coronal"),
58
+ cmap: str = "gray",
59
+ figsize: tuple[float, float] | None = None,
60
+ mask_style: str = "blank",
61
+ raw_mean: float | Tensor | None = None,
62
+ raw_std: float | Tensor | None = None,
63
+ ):
64
+ target_vol = _select_volume(target, sample_idx=sample_idx, channel_idx=channel_idx)
65
+ pred_vol = _select_volume(pred, sample_idx=sample_idx, channel_idx=channel_idx)
66
+ if raw_mean is not None and raw_std is not None:
67
+ raw_mean = _select_scalar(raw_mean, sample_idx=sample_idx)
68
+ raw_std = _select_scalar(raw_std, sample_idx=sample_idx)
69
+ target_vol = target_vol * raw_std + raw_mean
70
+ pred_vol = pred_vol * raw_std + raw_mean
71
+ pred_mask_vol = (
72
+ torch.zeros_like(target_vol)
73
+ if pred_mask is None
74
+ else _select_volume(pred_mask, sample_idx=sample_idx, channel_idx=channel_idx) > 0
75
+ )
76
+ img_mask_vol = None
77
+ if img_mask is not None:
78
+ img_mask_vol = _select_volume(img_mask, sample_idx=sample_idx, channel_idx=channel_idx) > 0
79
+
80
+ composite_vol = _prediction_composite(target_vol, pred_vol, pred_mask_vol)
81
+ vmin, vmax = _intensity_limits(target_vol, img_mask_vol)
82
+
83
+ patch_size = _as_3tuple(patch_size)
84
+ view_items = []
85
+ for view in views:
86
+ view_key = view.lower()
87
+ if view_key not in VIEW_NAMES:
88
+ raise ValueError(f"unknown MRI view {view!r}; expected one of {tuple(VIEW_NAMES)}")
89
+ target_slice = _extract_view_slice(target_vol, view_key, slice_idx)
90
+ composite_slice = _extract_view_slice(composite_vol, view_key, slice_idx)
91
+ mask_slice = _extract_view_slice(pred_mask_vol.float(), view_key, slice_idx) > 0
92
+ img_mask_slice = None
93
+ if img_mask_vol is not None:
94
+ img_mask_slice = _extract_view_slice(img_mask_vol.float(), view_key, slice_idx) > 0
95
+ view_items.append(
96
+ {
97
+ "key": view_key,
98
+ "title": VIEW_NAMES[view_key],
99
+ "target": _masked_input_display(target_slice, mask_slice, img_mask_slice, vmin),
100
+ "composite": _apply_display_mask(composite_slice, img_mask_slice, vmin),
101
+ "actual": _apply_display_mask(target_slice, img_mask_slice, vmin),
102
+ "mask": mask_slice,
103
+ "img_mask": img_mask_slice,
104
+ "patch_rc": _view_patch_size(view_key, patch_size),
105
+ }
106
+ )
107
+
108
+ _crop_view_items(view_items)
109
+
110
+ fig, axes, layout = _make_figure_canvas(view_items, figsize=figsize)
111
+ for item, x in zip(view_items, layout["col_centers"]):
112
+ fig.text(
113
+ x,
114
+ layout["title_y"],
115
+ item["title"],
116
+ ha="center",
117
+ va="center",
118
+ color="#f8fafc",
119
+ fontsize=7,
120
+ )
121
+ for label, y in zip(("Masked", "Pred", "Actual"), layout["row_centers"]):
122
+ fig.text(layout["label_x"], y, label, ha="right", va="center", color="#cbd5e1", fontsize=6)
123
+
124
+ for item, top_ax, middle_ax, bottom_ax in zip(view_items, axes[0], axes[1], axes[2]):
125
+ top_ax.imshow(
126
+ item["target"],
127
+ cmap=cmap,
128
+ vmin=vmin,
129
+ vmax=vmax,
130
+ interpolation="nearest",
131
+ origin="upper",
132
+ )
133
+ if mask_style == "boxes":
134
+ _draw_patch_boxes(top_ax, item["mask"], item["patch_rc"])
135
+ elif mask_style != "blank":
136
+ raise ValueError("mask_style must be 'blank' or 'boxes'")
137
+ _style_axis(top_ax)
138
+
139
+ middle_ax.imshow(
140
+ item["composite"],
141
+ cmap=cmap,
142
+ vmin=vmin,
143
+ vmax=vmax,
144
+ interpolation="nearest",
145
+ origin="upper",
146
+ )
147
+ _style_axis(middle_ax)
148
+
149
+ bottom_ax.imshow(
150
+ item["actual"],
151
+ cmap=cmap,
152
+ vmin=vmin,
153
+ vmax=vmax,
154
+ interpolation="nearest",
155
+ origin="upper",
156
+ )
157
+ _style_axis(bottom_ax)
158
+ return fig
159
+
160
+
161
+ def _select_volume(
162
+ x: Tensor,
163
+ sample_idx: int = 0,
164
+ channel_idx: int = 0,
165
+ ) -> Tensor:
166
+ x = x.detach().float().cpu()
167
+ if x.ndim == 5:
168
+ return x[sample_idx, channel_idx]
169
+ if x.ndim == 4:
170
+ return x[sample_idx]
171
+ if x.ndim == 3:
172
+ return x
173
+ raise ValueError(f"expected a 3D volume tensor, got shape {tuple(x.shape)}")
174
+
175
+
176
+ def _select_scalar(value: float | Tensor, sample_idx: int = 0) -> float:
177
+ if isinstance(value, Tensor):
178
+ value = value.detach().float().cpu()
179
+ if value.ndim > 0:
180
+ value = value.reshape(-1)[sample_idx]
181
+ return float(value)
182
+ return float(value)
183
+
184
+
185
+ def _prediction_composite(target: Tensor, pred: Tensor, pred_mask: Tensor) -> Tensor:
186
+ pred_mask = pred_mask.to(dtype=target.dtype)
187
+ return target * (1 - pred_mask) + pred * pred_mask
188
+
189
+
190
+ def _extract_view_slice(
191
+ volume: Tensor,
192
+ view: str,
193
+ slice_idx: int | Mapping[str, int] | None = None,
194
+ ) -> Tensor:
195
+ if isinstance(slice_idx, Mapping):
196
+ slice_idx = slice_idx.get(view)
197
+
198
+ if view in ("sagittal", "saggital"):
199
+ idx = _resolve_slice_idx(volume.shape[0], slice_idx)
200
+ return volume[idx, :, :].transpose(0, 1).flip(0)
201
+ if view == "axial":
202
+ idx = _resolve_slice_idx(volume.shape[2], slice_idx)
203
+ return volume[:, :, idx].transpose(0, 1).flip(0)
204
+ if view == "coronal":
205
+ idx = _resolve_slice_idx(volume.shape[1], slice_idx)
206
+ return volume[:, idx, :].transpose(0, 1).flip(0)
207
+ raise ValueError(f"unknown MRI view {view!r}")
208
+
209
+
210
+ def _resolve_slice_idx(size: int, slice_idx: int | None) -> int:
211
+ idx = size // 2 if slice_idx is None else int(slice_idx)
212
+ if idx < 0:
213
+ idx += size
214
+ if idx < 0 or idx >= size:
215
+ raise IndexError(f"slice index {idx} is out of bounds for axis with size {size}")
216
+ return idx
217
+
218
+
219
+ def _intensity_limits(volume: Tensor, mask: Tensor | None = None) -> tuple[float, float]:
220
+ values = volume[mask] if mask is not None and mask.any() else volume.flatten()
221
+ values = values[torch.isfinite(values)]
222
+ if values.numel() == 0:
223
+ return 0.0, 1.0
224
+ if values.numel() < 32:
225
+ vmin = values.min()
226
+ vmax = values.max()
227
+ else:
228
+ vmin, vmax = torch.quantile(values, torch.tensor([0.005, 0.995]))
229
+ if torch.isclose(vmin, vmax):
230
+ delta = max(abs(float(vmin)) * 0.05, 1.0)
231
+ return float(vmin) - delta, float(vmax) + delta
232
+ return float(vmin), float(vmax)
233
+
234
+
235
+ def _apply_display_mask(image: Tensor, mask: Tensor | None, fill_value: float) -> Tensor:
236
+ if mask is None:
237
+ return image
238
+ return torch.where(mask, image, torch.full_like(image, fill_value))
239
+
240
+
241
+ def _masked_input_display(
242
+ image: Tensor,
243
+ pred_mask: Tensor,
244
+ img_mask: Tensor | None,
245
+ fill_value: float,
246
+ ) -> Tensor:
247
+ display = torch.where(pred_mask, torch.full_like(image, fill_value), image)
248
+ return _apply_display_mask(display, img_mask, fill_value)
249
+
250
+
251
+ def _crop_view_items(view_items: list[dict]) -> None:
252
+ for item in view_items:
253
+ mask = item["img_mask"]
254
+ if mask is None:
255
+ mask = item["actual"] != item["actual"].min()
256
+ row_slice, col_slice = _content_crop(mask, item["patch_rc"])
257
+ for key in ("target", "composite", "actual", "mask"):
258
+ item[key] = item[key][row_slice, col_slice]
259
+ if item["img_mask"] is not None:
260
+ item["img_mask"] = item["img_mask"][row_slice, col_slice]
261
+
262
+
263
+ def _content_crop(mask: Tensor, patch_size: tuple[int, int]) -> tuple[slice, slice]:
264
+ mask = mask.detach().cpu() > 0
265
+ if not mask.any():
266
+ return slice(None), slice(None)
267
+
268
+ rows, cols = mask.nonzero(as_tuple=True)
269
+ patch_h, patch_w = patch_size
270
+ height, width = mask.shape
271
+ row0 = max((int(rows.min()) // patch_h - 1) * patch_h, 0)
272
+ row1 = min((int(rows.max()) // patch_h + 2) * patch_h, height)
273
+ col0 = max((int(cols.min()) // patch_w - 1) * patch_w, 0)
274
+ col1 = min((int(cols.max()) // patch_w + 2) * patch_w, width)
275
+ return slice(row0, row1), slice(col0, col1)
276
+
277
+
278
+ def _as_3tuple(value: int | tuple[int, int, int]) -> tuple[int, int, int]:
279
+ if isinstance(value, int):
280
+ return (value, value, value)
281
+ if len(value) != 3:
282
+ raise ValueError(f"expected a 3-tuple patch size, got {value!r}")
283
+ return tuple(int(v) for v in value)
284
+
285
+
286
+ def _view_patch_size(view: str, patch_size: tuple[int, int, int]) -> tuple[int, int]:
287
+ p_x, p_y, p_z = patch_size
288
+ if view in ("sagittal", "saggital"):
289
+ return p_z, p_y
290
+ if view == "axial":
291
+ return p_y, p_x
292
+ if view == "coronal":
293
+ return p_z, p_x
294
+ raise ValueError(f"unknown MRI view {view!r}")
295
+
296
+
297
+ def _make_figure_canvas(
298
+ view_items: list[dict[str, Tensor | str | tuple[int, int]]],
299
+ figsize: tuple[float, float] | None = None,
300
+ ):
301
+ dpi = 160
302
+ left = 58
303
+ right = 6
304
+ top = 18
305
+ bottom = 8
306
+ row_gap = 14
307
+ col_gap = 8
308
+ widths = [int(item["target"].shape[1]) for item in view_items]
309
+ heights = [int(item["target"].shape[0]) for item in view_items]
310
+ row_h = max(heights)
311
+ num_rows = 3
312
+ fig_w = left + right + sum(widths) + col_gap * (len(widths) - 1)
313
+ fig_h = top + bottom + row_h * num_rows + row_gap * (num_rows - 1)
314
+
315
+ scale = 1.35
316
+ if figsize is not None:
317
+ requested_w = figsize[0] * dpi
318
+ requested_h = figsize[1] * dpi
319
+ scale = max(requested_w / fig_w, requested_h / fig_h)
320
+ figsize = (fig_w * scale / dpi, fig_h * scale / dpi)
321
+ fig = plt.figure(figsize=figsize, dpi=dpi, facecolor="#0b0f14")
322
+
323
+ axes = [[] for _ in range(num_rows)]
324
+ col_centers = []
325
+ x = left
326
+ for width, height in zip(widths, heights):
327
+ col_centers.append((x + width / 2) / fig_w)
328
+ ys = [
329
+ bottom + (num_rows - row - 1) * (row_h + row_gap) + (row_h - height) / 2
330
+ for row in range(num_rows)
331
+ ]
332
+ for row, y in enumerate(ys):
333
+ axes[row].append(
334
+ fig.add_axes(
335
+ [
336
+ x / fig_w,
337
+ y / fig_h,
338
+ width / fig_w,
339
+ height / fig_h,
340
+ ],
341
+ facecolor="black",
342
+ )
343
+ )
344
+ x += width + col_gap
345
+ row_centers = [
346
+ (bottom + (num_rows - row - 1) * (row_h + row_gap) + row_h / 2) / fig_h
347
+ for row in range(num_rows)
348
+ ]
349
+ layout = {
350
+ "col_centers": col_centers,
351
+ "row_centers": row_centers,
352
+ "label_x": (left - 8) / fig_w,
353
+ "title_y": (fig_h - top / 2) / fig_h,
354
+ }
355
+
356
+ return fig, axes, layout
357
+
358
+
359
+ def _style_axis(ax) -> None:
360
+ ax.set_xticks([])
361
+ ax.set_yticks([])
362
+ for spine in ax.spines.values():
363
+ spine.set_visible(False)
364
+
365
+
366
+ def _draw_patch_boxes(
367
+ ax,
368
+ mask: Tensor,
369
+ patch_size: tuple[int, int],
370
+ color: str = "#facc15",
371
+ ) -> None:
372
+ for col, row, width, height in _patch_rectangles(mask, patch_size):
373
+ ax.add_patch(
374
+ patches.Rectangle(
375
+ (col - 0.5, row - 0.5),
376
+ width,
377
+ height,
378
+ fill=False,
379
+ edgecolor=color,
380
+ linewidth=0.75,
381
+ alpha=0.95,
382
+ )
383
+ )
384
+
385
+
386
+ def _patch_rectangles(
387
+ mask: Tensor,
388
+ patch_size: tuple[int, int],
389
+ ) -> list[tuple[int, int, int, int]]:
390
+ mask = mask.detach().cpu() > 0
391
+ patch_h, patch_w = patch_size
392
+ height, width = mask.shape
393
+ rectangles = []
394
+ for row in range(0, height, patch_h):
395
+ box_h = min(patch_h, height - row)
396
+ for col in range(0, width, patch_w):
397
+ box_w = min(patch_w, width - col)
398
+ if mask[row : row + box_h, col : col + box_w].any():
399
+ rectangles.append((col, row, box_w, box_h))
400
+ return rectangles
finetune/fomo_tune_baseline/output/task5/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task5
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task5
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/output/task5/log.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 16:32:19 run task5 (git 1df2e5d)
2
+ 16:32:19 config:
3
+ task: task5
4
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
5
+ output_root: experiments/fomo_tune_baseline/output
6
+ name: task5
7
+ device: cuda
8
+ seed: 4466
9
+ 16:32:42 dataset: 48 subjects, 24 positive
10
+ 16:33:21 fold 1/20 n=3 y=[0 1 1] p=[0.013 0.998 0.974] (35s)
11
+ 16:33:24 fold 2/20 n=3 y=[1 1 1] p=[0.796 0.719 0.999] (37s)
12
+ 16:33:26 fold 3/20 n=3 y=[0 1 1] p=[0.155 0.998 0.905] (39s)
13
+ 16:33:28 fold 4/20 n=3 y=[0 0 1] p=[0.024 0.032 0.811] (42s)
14
+ 16:33:29 fold 5/20 n=3 y=[1 1 1] p=[0.988 0.652 0.552] (43s)
15
+ 16:33:31 fold 6/20 n=3 y=[0 0 1] p=[0.46 0.744 0.994] (44s)
16
+ 16:33:33 fold 7/20 n=3 y=[0 0 1] p=[0.134 0.148 0.996] (47s)
17
+ 16:33:36 fold 8/20 n=3 y=[0 0 1] p=[0.037 0.012 0.915] (50s)
18
+ 16:33:37 fold 9/20 n=2 y=[0 1] p=[0.137 0.997] (51s)
19
+ 16:33:38 fold 10/20 n=2 y=[0 1] p=[0.223 0.993] (51s)
20
+ 16:33:39 fold 11/20 n=2 y=[0 0] p=[0.118 0.32 ] (53s)
21
+ 16:33:42 fold 12/20 n=2 y=[0 1] p=[0.122 0.849] (55s)
22
+ 16:33:43 fold 13/20 n=2 y=[0 1] p=[0.207 0.282] (57s)
23
+ 16:33:45 fold 14/20 n=2 y=[0 1] p=[0.177 0.319] (58s)
24
+ 16:33:47 fold 15/20 n=2 y=[0 1] p=[0.112 0.943] (60s)
25
+ 16:33:48 fold 16/20 n=2 y=[0 1] p=[0.034 0.998] (61s)
26
+ 16:33:50 fold 17/20 n=2 y=[0 0] p=[0.018 0.021] (63s)
27
+ 16:33:51 fold 18/20 n=2 y=[0 1] p=[0.02 0.937] (64s)
28
+ 16:33:52 fold 19/20 n=2 y=[0 1] p=[0.004 0.96 ] (65s)
29
+ 16:33:54 fold 20/20 n=2 y=[0 1] p=[0.075 0.929] (68s)
30
+ 16:33:56 result: auroc=0.9844 auroc_ci_low=0.9528 auroc_ci_high=1.0000 (68s)
finetune/fomo_tune_baseline/output/task5/metrics.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name": "task5", "auroc": 0.984375, "auroc_ci_low": 0.9527867965367964, "auroc_ci_high": 1.0, "run_time": 67.5}
finetune/fomo_tune_baseline/output/task5/model/config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ task: task5
2
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
3
+ output_root: experiments/fomo_tune_baseline/output
4
+ name: task5
5
+ device: cuda
6
+ seed: 4466
finetune/fomo_tune_baseline/slurms/build.log ADDED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === task1 ===
2
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
3
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
4
+ INFO: Starting build...
5
+ INFO: Fetching OCI image...
6
+ INFO: Extracting OCI image...
7
+ INFO: Inserting Apptainer configuration...
8
+ INFO: Copying fomo_tune to /app/fomo_tune
9
+ INFO: Copying smri_mae to /app/smri_mae
10
+ INFO: Copying model to /app/model
11
+ INFO: Copying predict.py to /app/predict.py
12
+ INFO: Running post scriptlet
13
+ + pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
14
+ Collecting torch==2.8.0
15
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
16
+ Collecting numpy==2.4.6
17
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
18
+ Collecting nibabel==5.4.2
19
+ Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
20
+ Collecting einops==0.8.2
21
+ Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
22
+ Collecting jaxtyping==0.3.10
23
+ Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
24
+ Collecting timm==1.0.27
25
+ Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
26
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 115.4 MB/s eta 0:00:00
27
+ Collecting huggingface-hub==0.36.2
28
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
29
+ Collecting scikit-learn==1.8.0
30
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
31
+ Collecting joblib==1.5.3
32
+ Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
33
+ Collecting omegaconf==2.3.0
34
+ Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
35
+ Collecting filelock (from torch==2.8.0)
36
+ Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
37
+ Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
38
+ Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
39
+ Collecting sympy>=1.13.3 (from torch==2.8.0)
40
+ Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
41
+ Collecting networkx (from torch==2.8.0)
42
+ Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
43
+ Collecting jinja2 (from torch==2.8.0)
44
+ Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
45
+ Collecting fsspec (from torch==2.8.0)
46
+ Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
47
+ Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
48
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
49
+ Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
50
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
51
+ Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
52
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
53
+ Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
54
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
55
+ Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
56
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
57
+ Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
58
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
59
+ Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
60
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
61
+ Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
62
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
63
+ Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
64
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
65
+ Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
66
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
67
+ Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
68
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
69
+ Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
70
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
71
+ Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
72
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
73
+ Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
74
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
75
+ Collecting triton==3.4.0 (from torch==2.8.0)
76
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
77
+ Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
78
+ Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
79
+ Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
80
+ Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
81
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
82
+ Collecting torchvision (from timm==1.0.27)
83
+ Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
84
+ Collecting pyyaml (from timm==1.0.27)
85
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
86
+ Collecting safetensors (from timm==1.0.27)
87
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
88
+ Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
89
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
90
+ Collecting requests (from huggingface-hub==0.36.2)
91
+ Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
92
+ Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
93
+ Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 49.5 MB/s eta 0:00:00
95
+ Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
96
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
97
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 59.8 MB/s eta 0:00:00
98
+ Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
99
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
100
+ Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
101
+ Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
102
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 53.3 MB/s eta 0:00:00
103
+ Preparing metadata (setup.py): started
104
+ Preparing metadata (setup.py): finished with status 'done'
105
+ Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
106
+ Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
107
+ Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
108
+ Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
109
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
110
+ Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
111
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
112
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 322.5 MB/s eta 0:00:00
113
+ Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
114
+ Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
115
+ Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
116
+ Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
117
+ Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
118
+ Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
119
+ INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
120
+ Collecting torchvision (from timm==1.0.27)
121
+ Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
122
+ Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
123
+ Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
124
+ Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
125
+ Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
126
+ Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
127
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
128
+ Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
129
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
130
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
131
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 227.6 MB/s eta 0:00:00
132
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
133
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 286.0 MB/s eta 0:00:00
134
+ Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
135
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 288.9 MB/s eta 0:00:00
136
+ Downloading einops-0.8.2-py3-none-any.whl (65 kB)
137
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 290.5 MB/s eta 0:00:00
138
+ Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
139
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 356.7 MB/s eta 0:00:00
140
+ Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
141
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 215.7 MB/s eta 0:00:00
142
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
143
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 221.4 MB/s eta 0:00:00
144
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
145
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 169.5 MB/s eta 0:00:00
146
+ Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
147
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 112.6 MB/s eta 0:00:00
148
+ Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
149
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 406.4 MB/s eta 0:00:00
150
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
151
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 213.7 MB/s eta 0:00:00
152
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
153
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 218.2 MB/s eta 0:00:00
154
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
155
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 301.5 MB/s eta 0:00:00
156
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
157
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 223.9 MB/s eta 0:00:00
158
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
159
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 302.6 MB/s eta 0:00:00
160
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 305.5 MB/s eta 0:00:00
162
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
163
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 228.2 MB/s eta 0:00:00
164
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
165
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 312.1 MB/s eta 0:00:00
166
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
167
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 301.3 MB/s eta 0:00:00
168
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
169
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 299.6 MB/s eta 0:00:00
170
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
171
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 304.3 MB/s eta 0:00:00
172
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
173
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 275.0 MB/s eta 0:00:00
174
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
175
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━���━━━ 39.3/39.3 MB 255.2 MB/s eta 0:00:00
176
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
177
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 393.8 MB/s eta 0:00:00
178
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
179
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 302.6 MB/s eta 0:00:00
180
+ Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
181
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 464.9 MB/s eta 0:00:00
182
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
183
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 338.8 MB/s eta 0:00:00
184
+ Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
185
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
186
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 427.8 MB/s eta 0:00:00
187
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
188
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 268.4 MB/s eta 0:00:00
189
+ Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
190
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 331.0 MB/s eta 0:00:00
191
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
192
+ Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
193
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 319.2 MB/s eta 0:00:00
194
+ Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
195
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 289.4 MB/s eta 0:00:00
196
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
197
+ Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
198
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 317.4 MB/s eta 0:00:00
199
+ Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
200
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 406.2 MB/s eta 0:00:00
201
+ Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
202
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 220.8 MB/s eta 0:00:00
203
+ Downloading requests-2.34.2-py3-none-any.whl (73 kB)
204
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 367.4 MB/s eta 0:00:00
205
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
206
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 227.7 MB/s eta 0:00:00
207
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
208
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 168.8 MB/s eta 0:00:00
209
+ Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
210
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 412.0 MB/s eta 0:00:00
211
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
212
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 455.8 MB/s eta 0:00:00
213
+ Downloading idna-3.18-py3-none-any.whl (65 kB)
214
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 258.9 MB/s eta 0:00:00
215
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
216
+ Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
217
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 241.6 MB/s eta 0:00:00
218
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
219
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 176.0 MB/s eta 0:00:00
220
+ Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
221
+ ━━━━━━���━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 349.6 MB/s eta 0:00:00
222
+ Building wheels for collected packages: antlr4-python3-runtime
223
+ Building wheel for antlr4-python3-runtime (setup.py): started
224
+ Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
225
+ Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=7049f12da321fd7083d3bdaa9af713223bfc3702787beff8fbc0980b4dfdc5a8
226
+ Stored in directory: /tmp/pip-ephem-wheel-cache-1binwsnn/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
227
+ Successfully built antlr4-python3-runtime
228
+ Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
229
+ Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
230
+ WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
231
+
232
+ [notice] A new release of pip is available: 24.0 -> 26.2.1
233
+ [notice] To update, run: pip install --upgrade pip
234
+ INFO: Adding runscript
235
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
236
+ INFO: Creating SIF file...
237
+ INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task1/task1.sif
238
+ built experiments/fomo_tune_baseline/output/task1/task1.sif
239
+ === task5 ===
240
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
241
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
242
+ INFO: Starting build...
243
+ INFO: Fetching OCI image...
244
+ INFO: Extracting OCI image...
245
+ INFO: Inserting Apptainer configuration...
246
+ INFO: Copying fomo_tune to /app/fomo_tune
247
+ INFO: Copying smri_mae to /app/smri_mae
248
+ INFO: Copying model to /app/model
249
+ INFO: Copying predict.py to /app/predict.py
250
+ INFO: Running post scriptlet
251
+ + pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
252
+ Collecting torch==2.8.0
253
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
254
+ Collecting numpy==2.4.6
255
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
256
+ Collecting nibabel==5.4.2
257
+ Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
258
+ Collecting einops==0.8.2
259
+ Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
260
+ Collecting jaxtyping==0.3.10
261
+ Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
262
+ Collecting timm==1.0.27
263
+ Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
264
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 20.8 MB/s eta 0:00:00
265
+ Collecting huggingface-hub==0.36.2
266
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
267
+ Collecting scikit-learn==1.8.0
268
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
269
+ Collecting joblib==1.5.3
270
+ Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
271
+ Collecting omegaconf==2.3.0
272
+ Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
273
+ Collecting filelock (from torch==2.8.0)
274
+ Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
275
+ Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
276
+ Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
277
+ Collecting sympy>=1.13.3 (from torch==2.8.0)
278
+ Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
279
+ Collecting networkx (from torch==2.8.0)
280
+ Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
281
+ Collecting jinja2 (from torch==2.8.0)
282
+ Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
283
+ Collecting fsspec (from torch==2.8.0)
284
+ Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
285
+ Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
286
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
287
+ Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
288
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
289
+ Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
290
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
291
+ Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
292
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
293
+ Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
294
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
295
+ Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
296
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
297
+ Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
298
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
299
+ Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
300
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
301
+ Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
302
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
303
+ Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
304
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
305
+ Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
306
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
307
+ Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
308
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
309
+ Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
310
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
311
+ Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
312
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
313
+ Collecting triton==3.4.0 (from torch==2.8.0)
314
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
315
+ Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
316
+ Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
317
+ Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
318
+ Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
319
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
320
+ Collecting torchvision (from timm==1.0.27)
321
+ Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
322
+ Collecting pyyaml (from timm==1.0.27)
323
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
324
+ Collecting safetensors (from timm==1.0.27)
325
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
326
+ Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
327
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
328
+ Collecting requests (from huggingface-hub==0.36.2)
329
+ Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
330
+ Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
331
+ Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
332
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 39.7 MB/s eta 0:00:00
333
+ Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
334
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
335
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 67.7 MB/s eta 0:00:00
336
+ Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
337
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
338
+ Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
339
+ Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
340
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 86.0 MB/s eta 0:00:00
341
+ Preparing metadata (setup.py): started
342
+ Preparing metadata (setup.py): finished with status 'done'
343
+ Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
344
+ Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
345
+ Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
346
+ Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
347
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
348
+ Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
349
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
350
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 295.5 MB/s eta 0:00:00
351
+ Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
352
+ Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
353
+ Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
354
+ Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
355
+ Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
356
+ Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
357
+ INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
358
+ Collecting torchvision (from timm==1.0.27)
359
+ Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
360
+ Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
361
+ Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
362
+ Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
363
+ Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
364
+ Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
365
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
366
+ Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
367
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
368
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
369
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 212.0 MB/s eta 0:00:00
370
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
371
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 311.3 MB/s eta 0:00:00
372
+ Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
373
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 210.5 MB/s eta 0:00:00
374
+ Downloading einops-0.8.2-py3-none-any.whl (65 kB)
375
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 354.1 MB/s eta 0:00:00
376
+ Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
377
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 336.3 MB/s eta 0:00:00
378
+ Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
379
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 175.2 MB/s eta 0:00:00
380
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
381
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 129.1 MB/s eta 0:00:00
382
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
383
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 308.6 MB/s eta 0:00:00
384
+ Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
385
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 431.8 MB/s eta 0:00:00
386
+ Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
387
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 388.1 MB/s eta 0:00:00
388
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
389
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 139.4 MB/s eta 0:00:00
390
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
391
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 174.8 MB/s eta 0:00:00
392
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
393
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 244.3 MB/s eta 0:00:00
394
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
395
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 289.0 MB/s eta 0:00:00
396
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
397
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 232.5 MB/s eta 0:00:00
398
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
399
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 155.1 MB/s eta 0:00:00
400
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
401
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 173.3 MB/s eta 0:00:00
402
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
403
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 280.8 MB/s eta 0:00:00
404
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
405
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 206.9 MB/s eta 0:00:00
406
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
407
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 178.9 MB/s eta 0:00:00
408
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
409
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 219.2 MB/s eta 0:00:00
410
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
411
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 296.8 MB/s eta 0:00:00
412
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
413
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 39.3/39.3 MB 207.8 MB/s eta 0:00:00
414
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
415
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 401.1 MB/s eta 0:00:00
416
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
417
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 299.9 MB/s eta 0:00:00
418
+ Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
419
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 461.6 MB/s eta 0:00:00
420
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
421
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 305.4 MB/s eta 0:00:00
422
+ Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
423
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
424
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 461.9 MB/s eta 0:00:00
425
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
426
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 157.6 MB/s eta 0:00:00
427
+ Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
428
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 254.2 MB/s eta 0:00:00
429
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
430
+ Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
431
+ ━━━━━━━━━━━━━━━��━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 384.1 MB/s eta 0:00:00
432
+ Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
433
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 314.4 MB/s eta 0:00:00
434
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
435
+ Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
436
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 308.0 MB/s eta 0:00:00
437
+ Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
438
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 357.5 MB/s eta 0:00:00
439
+ Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
440
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 327.2 MB/s eta 0:00:00
441
+ Downloading requests-2.34.2-py3-none-any.whl (73 kB)
442
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 379.7 MB/s eta 0:00:00
443
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
444
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 201.6 MB/s eta 0:00:00
445
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
446
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 189.9 MB/s eta 0:00:00
447
+ Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
448
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 390.2 MB/s eta 0:00:00
449
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
450
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 440.2 MB/s eta 0:00:00
451
+ Downloading idna-3.18-py3-none-any.whl (65 kB)
452
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 278.9 MB/s eta 0:00:00
453
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
454
+ Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
455
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 470.6 MB/s eta 0:00:00
456
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
457
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 295.5 MB/s eta 0:00:00
458
+ Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
459
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 434.9 MB/s eta 0:00:00
460
+ Building wheels for collected packages: antlr4-python3-runtime
461
+ Building wheel for antlr4-python3-runtime (setup.py): started
462
+ Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
463
+ Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=9153c9732e66786ceb0134b28a0118f6e9fedc66584880fabf6533f1989fac2e
464
+ Stored in directory: /tmp/pip-ephem-wheel-cache-5dznrpve/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
465
+ Successfully built antlr4-python3-runtime
466
+ Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
467
+ Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
468
+ WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
469
+
470
+ [notice] A new release of pip is available: 24.0 -> 26.2.1
471
+ [notice] To update, run: pip install --upgrade pip
472
+ INFO: Adding runscript
473
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
474
+ INFO: Creating SIF file...
475
+ INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task5/task5.sif
476
+ built experiments/fomo_tune_baseline/output/task5/task5.sif
477
+ === task3 ===
478
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
479
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
480
+ INFO: Starting build...
481
+ INFO: Fetching OCI image...
482
+ INFO: Extracting OCI image...
483
+ INFO: Inserting Apptainer configuration...
484
+ INFO: Copying fomo_tune to /app/fomo_tune
485
+ INFO: Copying smri_mae to /app/smri_mae
486
+ INFO: Copying model to /app/model
487
+ INFO: Copying predict.py to /app/predict.py
488
+ INFO: Running post scriptlet
489
+ + pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
490
+ Collecting torch==2.8.0
491
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
492
+ Collecting numpy==2.4.6
493
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
494
+ Collecting nibabel==5.4.2
495
+ Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
496
+ Collecting einops==0.8.2
497
+ Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
498
+ Collecting jaxtyping==0.3.10
499
+ Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
500
+ Collecting timm==1.0.27
501
+ Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
502
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 330.8 MB/s eta 0:00:00
503
+ Collecting huggingface-hub==0.36.2
504
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
505
+ Collecting scikit-learn==1.8.0
506
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
507
+ Collecting joblib==1.5.3
508
+ Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
509
+ Collecting omegaconf==2.3.0
510
+ Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
511
+ Collecting filelock (from torch==2.8.0)
512
+ Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
513
+ Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
514
+ Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
515
+ Collecting sympy>=1.13.3 (from torch==2.8.0)
516
+ Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
517
+ Collecting networkx (from torch==2.8.0)
518
+ Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
519
+ Collecting jinja2 (from torch==2.8.0)
520
+ Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
521
+ Collecting fsspec (from torch==2.8.0)
522
+ Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
523
+ Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
524
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
525
+ Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
526
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
527
+ Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
528
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
529
+ Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
530
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
531
+ Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
532
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
533
+ Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
534
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
535
+ Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
536
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
537
+ Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
538
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
539
+ Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
540
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
541
+ Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
542
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
543
+ Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
544
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
545
+ Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
546
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
547
+ Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
548
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
549
+ Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
550
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
551
+ Collecting triton==3.4.0 (from torch==2.8.0)
552
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
553
+ Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
554
+ Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
555
+ Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
556
+ Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
557
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
558
+ Collecting torchvision (from timm==1.0.27)
559
+ Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
560
+ Collecting pyyaml (from timm==1.0.27)
561
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
562
+ Collecting safetensors (from timm==1.0.27)
563
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
564
+ Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
565
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
566
+ Collecting requests (from huggingface-hub==0.36.2)
567
+ Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
568
+ Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
569
+ Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
570
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 362.2 MB/s eta 0:00:00
571
+ Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
572
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
573
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 394.1 MB/s eta 0:00:00
574
+ Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
575
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
576
+ Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
577
+ Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
578
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 424.4 MB/s eta 0:00:00
579
+ Preparing metadata (setup.py): started
580
+ Preparing metadata (setup.py): finished with status 'done'
581
+ Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
582
+ Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
583
+ Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
584
+ Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
585
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
586
+ Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
587
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
588
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 330.0 MB/s eta 0:00:00
589
+ Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
590
+ Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
591
+ Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
592
+ Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
593
+ Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
594
+ Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
595
+ INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
596
+ Collecting torchvision (from timm==1.0.27)
597
+ Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
598
+ Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
599
+ Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
600
+ Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
601
+ Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
602
+ Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
603
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
604
+ Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
605
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
606
+ Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
607
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 183.5 MB/s eta 0:00:00
608
+ Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
609
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 258.7 MB/s eta 0:00:00
610
+ Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
611
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 227.2 MB/s eta 0:00:00
612
+ Downloading einops-0.8.2-py3-none-any.whl (65 kB)
613
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 281.2 MB/s eta 0:00:00
614
+ Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
615
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 341.7 MB/s eta 0:00:00
616
+ Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
617
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 220.8 MB/s eta 0:00:00
618
+ Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
619
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 451.6 MB/s eta 0:00:00
620
+ Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
621
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 264.0 MB/s eta 0:00:00
622
+ Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
623
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 480.7 MB/s eta 0:00:00
624
+ Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
625
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 377.4 MB/s eta 0:00:00
626
+ Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
627
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 261.8 MB/s eta 0:00:00
628
+ Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
629
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 212.3 MB/s eta 0:00:00
630
+ Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
631
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 224.5 MB/s eta 0:00:00
632
+ Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
633
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 309.2 MB/s eta 0:00:00
634
+ Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
635
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 361.1 MB/s eta 0:00:00
636
+ Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
637
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 210.5 MB/s eta 0:00:00
638
+ Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
639
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 406.4 MB/s eta 0:00:00
640
+ Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
641
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 339.4 MB/s eta 0:00:00
642
+ Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
643
+ ━━━━━━━━━━━━━━━━��━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 292.8 MB/s eta 0:00:00
644
+ Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
645
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 291.6 MB/s eta 0:00:00
646
+ Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
647
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 301.2 MB/s eta 0:00:00
648
+ Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
649
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 221.2 MB/s eta 0:00:00
650
+ Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
651
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 39.3/39.3 MB 343.4 MB/s eta 0:00:00
652
+ Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
653
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 389.6 MB/s eta 0:00:00
654
+ Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
655
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 206.4 MB/s eta 0:00:00
656
+ Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
657
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 369.1 MB/s eta 0:00:00
658
+ Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
659
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 376.2 MB/s eta 0:00:00
660
+ Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
661
+ Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
662
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 457.9 MB/s eta 0:00:00
663
+ Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
664
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 210.6 MB/s eta 0:00:00
665
+ Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
666
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 365.8 MB/s eta 0:00:00
667
+ Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
668
+ Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
669
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 406.0 MB/s eta 0:00:00
670
+ Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
671
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 218.5 MB/s eta 0:00:00
672
+ Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
673
+ Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
674
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 321.0 MB/s eta 0:00:00
675
+ Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
676
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 418.1 MB/s eta 0:00:00
677
+ Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
678
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 377.2 MB/s eta 0:00:00
679
+ Downloading requests-2.34.2-py3-none-any.whl (73 kB)
680
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 298.4 MB/s eta 0:00:00
681
+ Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
682
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 453.2 MB/s eta 0:00:00
683
+ Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
684
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 168.6 MB/s eta 0:00:00
685
+ Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
686
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 368.6 MB/s eta 0:00:00
687
+ Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
688
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 471.0 MB/s eta 0:00:00
689
+ Downloading idna-3.18-py3-none-any.whl (65 kB)
690
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 392.2 MB/s eta 0:00:00
691
+ Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
692
+ Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
693
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 478.8 MB/s eta 0:00:00
694
+ Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
695
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 369.2 MB/s eta 0:00:00
696
+ Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
697
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 438.3 MB/s eta 0:00:00
698
+ Building wheels for collected packages: antlr4-python3-runtime
699
+ Building wheel for antlr4-python3-runtime (setup.py): started
700
+ Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
701
+ Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=5aa799b6efbe9e55190ab92e61a9ecd9b458a6b893b508bf688fa3754a709a14
702
+ Stored in directory: /tmp/pip-ephem-wheel-cache-pem62r8t/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
703
+ Successfully built antlr4-python3-runtime
704
+ Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
705
+ Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
706
+ WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
707
+
708
+ [notice] A new release of pip is available: 24.0 -> 26.2.1
709
+ [notice] To update, run: pip install --upgrade pip
710
+ INFO: Adding runscript
711
+ INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
712
+ INFO: Creating SIF file...
713
+ INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task3/task3.sif
714
+ built experiments/fomo_tune_baseline/output/task3/task3.sif
715
+ === sifs ===
716
+ -rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:48 experiments/fomo_tune_baseline/output/task1/task1.sif
717
+ -rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:58 experiments/fomo_tune_baseline/output/task3/task3.sif
718
+ -rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:53 experiments/fomo_tune_baseline/output/task5/task5.sif
finetune/fomo_tune_baseline/slurms/slurm-378508.out ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === task1 ===
2
+ 16:31:38 run task1 (git 1df2e5d)
3
+ 16:31:38 config:
4
+ task: task1
5
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
6
+ modalities:
7
+ - dwi_b1000
8
+ output_root: experiments/fomo_tune_baseline/output
9
+ name: task1
10
+ device: cuda
11
+ seed: 4466
12
+ 16:31:40 dataset: 21 subjects, 13 positive
13
+ 16:31:48 fold 1/21 sub-01 y=1 p=0.512 (4s)
14
+ 16:31:49 fold 2/21 sub-02 y=1 p=0.514 (4s)
15
+ 16:31:49 fold 3/21 sub-03 y=0 p=0.486 (5s)
16
+ 16:31:49 fold 4/21 sub-04 y=1 p=0.499 (5s)
17
+ 16:31:50 fold 5/21 sub-05 y=1 p=0.507 (5s)
18
+ 16:31:50 fold 6/21 sub-06 y=1 p=0.493 (6s)
19
+ 16:31:50 fold 7/21 sub-07 y=0 p=0.489 (6s)
20
+ 16:31:51 fold 8/21 sub-08 y=0 p=0.490 (6s)
21
+ 16:31:51 fold 9/21 sub-09 y=1 p=0.523 (7s)
22
+ 16:31:52 fold 10/21 sub-10 y=1 p=0.525 (7s)
23
+ 16:31:52 fold 11/21 sub-11 y=0 p=0.479 (7s)
24
+ 16:31:52 fold 12/21 sub-12 y=0 p=0.474 (8s)
25
+ 16:31:53 fold 13/21 sub-13 y=1 p=0.516 (8s)
26
+ 16:31:53 fold 14/21 sub-14 y=1 p=0.536 (9s)
27
+ 16:31:53 fold 15/21 sub-15 y=0 p=0.486 (9s)
28
+ 16:31:54 fold 16/21 sub-16 y=0 p=0.482 (9s)
29
+ 16:31:54 fold 17/21 sub-17 y=1 p=0.520 (10s)
30
+ 16:31:54 fold 18/21 sub-18 y=1 p=0.518 (10s)
31
+ 16:31:55 fold 19/21 sub-19 y=1 p=0.520 (10s)
32
+ 16:31:55 fold 20/21 sub-20 y=1 p=0.519 (11s)
33
+ 16:31:56 fold 21/21 sub-21 y=0 p=0.498 (11s)
34
+ 16:31:57 result: auroc=0.9904 auroc_ci_low=0.9444 auroc_ci_high=1.0000 (11s)
35
+ === task5 ===
36
+ 16:32:19 run task5 (git 1df2e5d)
37
+ 16:32:19 config:
38
+ task: task5
39
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
40
+ output_root: experiments/fomo_tune_baseline/output
41
+ name: task5
42
+ device: cuda
43
+ seed: 4466
44
+ 16:32:42 dataset: 48 subjects, 24 positive
45
+ 16:33:21 fold 1/20 n=3 y=[0 1 1] p=[0.013 0.998 0.974] (35s)
46
+ 16:33:24 fold 2/20 n=3 y=[1 1 1] p=[0.796 0.719 0.999] (37s)
47
+ 16:33:26 fold 3/20 n=3 y=[0 1 1] p=[0.155 0.998 0.905] (39s)
48
+ 16:33:28 fold 4/20 n=3 y=[0 0 1] p=[0.024 0.032 0.811] (42s)
49
+ 16:33:29 fold 5/20 n=3 y=[1 1 1] p=[0.988 0.652 0.552] (43s)
50
+ 16:33:31 fold 6/20 n=3 y=[0 0 1] p=[0.46 0.744 0.994] (44s)
51
+ 16:33:33 fold 7/20 n=3 y=[0 0 1] p=[0.134 0.148 0.996] (47s)
52
+ 16:33:36 fold 8/20 n=3 y=[0 0 1] p=[0.037 0.012 0.915] (50s)
53
+ 16:33:37 fold 9/20 n=2 y=[0 1] p=[0.137 0.997] (51s)
54
+ 16:33:38 fold 10/20 n=2 y=[0 1] p=[0.223 0.993] (51s)
55
+ 16:33:39 fold 11/20 n=2 y=[0 0] p=[0.118 0.32 ] (53s)
56
+ 16:33:42 fold 12/20 n=2 y=[0 1] p=[0.122 0.849] (55s)
57
+ 16:33:43 fold 13/20 n=2 y=[0 1] p=[0.207 0.282] (57s)
58
+ 16:33:45 fold 14/20 n=2 y=[0 1] p=[0.177 0.319] (58s)
59
+ 16:33:47 fold 15/20 n=2 y=[0 1] p=[0.112 0.943] (60s)
60
+ 16:33:48 fold 16/20 n=2 y=[0 1] p=[0.034 0.998] (61s)
61
+ 16:33:50 fold 17/20 n=2 y=[0 0] p=[0.018 0.021] (63s)
62
+ 16:33:51 fold 18/20 n=2 y=[0 1] p=[0.02 0.937] (64s)
63
+ 16:33:52 fold 19/20 n=2 y=[0 1] p=[0.004 0.96 ] (65s)
64
+ 16:33:54 fold 20/20 n=2 y=[0 1] p=[0.075 0.929] (68s)
65
+ 16:33:56 result: auroc=0.9844 auroc_ci_low=0.9528 auroc_ci_high=1.0000 (68s)
66
+ === task3 ===
67
+ 16:34:12 run task3 (git 1df2e5d)
68
+ 16:34:12 config:
69
+ task: task3
70
+ ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
71
+ output_root: experiments/fomo_tune_baseline/output
72
+ name: task3
73
+ device: cuda
74
+ seed: 4466
75
+ 16:35:16 dataset: 494 subjects, age 19-80 mean 45.2
76
+ 16:38:39 fold 1/20 n=25 mae=3.91 (190s)
77
+ 16:38:51 fold 2/20 n=25 mae=4.50 (202s)
78
+ 16:38:57 fold 3/20 n=25 mae=3.74 (208s)
79
+ 16:39:03 fold 4/20 n=25 mae=3.58 (214s)
80
+ 16:39:09 fold 5/20 n=25 mae=3.57 (220s)
81
+ 16:39:14 fold 6/20 n=25 mae=3.12 (226s)
82
+ 16:39:20 fold 7/20 n=25 mae=3.42 (232s)
83
+ 16:39:26 fold 8/20 n=25 mae=3.12 (238s)
84
+ 16:39:32 fold 9/20 n=25 mae=3.74 (244s)
85
+ 16:39:38 fold 10/20 n=25 mae=4.20 (249s)
86
+ 16:39:44 fold 11/20 n=25 mae=3.22 (255s)
87
+ 16:39:50 fold 12/20 n=25 mae=3.50 (261s)
88
+ 16:39:55 fold 13/20 n=25 mae=3.39 (267s)
89
+ 16:40:01 fold 14/20 n=25 mae=4.17 (273s)
90
+ 16:40:07 fold 15/20 n=24 mae=4.36 (278s)
91
+ 16:40:12 fold 16/20 n=24 mae=4.15 (284s)
92
+ 16:40:18 fold 17/20 n=24 mae=3.66 (289s)
93
+ 16:40:23 fold 18/20 n=24 mae=3.54 (295s)
94
+ 16:40:29 fold 19/20 n=24 mae=3.42 (300s)
95
+ 16:40:34 fold 20/20 n=24 mae=3.53 (306s)
96
+ 16:40:35 result: pearson_r=0.9631 pearson_r_ci_low=0.9567 pearson_r_ci_high=0.9687 mae=3.6910 mae_ci_low=3.4464 mae_ci_high=3.9466 (306s)
97
+ === results ===
98
+ {"name": "task1", "auroc": 0.9903846153846154, "auroc_ci_low": 0.9444444444444445, "auroc_ci_high": 1.0, "run_time": 11.2}
99
+ {"name": "task3", "pearson_r": 0.9630771532969529, "pearson_r_ci_low": 0.9567144985968425, "pearson_r_ci_high": 0.9687044710309238, "mae": 3.6910024558001684, "mae_ci_low": 3.446356968454986, "mae_ci_high": 3.946628273643462, "run_time": 306.1}
100
+ {"name": "task5", "auroc": 0.984375, "auroc_ci_low": 0.9527867965367964, "auroc_ci_high": 1.0, "run_time": 67.5}