Add LoRA fine-tuning module for Qwen models
- Implement LoRA fine-tuning for Qwen-family models (Qwen2, Qwen2.5, Qwen3) - Add new `finetune.rs` with full training pipeline and LoRA injection - Update dependencies: add candle-core, candle-nn, tokenizers, serde, serde_json - Add FineTune screen and focus state to app and UI modules
keypair34 committed
Apr 22, 2026 at 16:32 UTC
4ce17f2e0c593d799efb297610b742c9edd9d0d7
6 files changed
+2696
-54
Cargo.lock
+1358
-53
@@ -2,6 +2,26 @@
2
# It is not intended for manual editing.
3
version = 4
4
5
+[[package]]
6
+name = "accelerate-src"
7
+version = "0.3.2"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "415ed64958754dbe991900f3940677e6a7eefb4d7367afd70d642677b0c7d19d"
10
+
11
+[[package]]
12
+name = "ahash"
13
+version = "0.8.12"
14
+source = "registry+https://github.com/rust-lang/crates.io-index"
15
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
16
+dependencies = [
17
+ "cfg-if",
18
+ "getrandom 0.3.4",
19
+ "once_cell",
20
+ "serde",
21
+ "version_check",
22
+ "zerocopy",
23
+]
24
+
25
[[package]]
26
name = "aho-corasick"
27
version = "1.1.4"
@@ -103,7 +123,7 @@ version = "0.2.14"
123
source = "registry+https://github.com/rust-lang/crates.io-index"
124
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
125
dependencies = [
106
- "hermit-abi",
126
+ "hermit-abi 0.1.19",
127
"libc",
128
"winapi",
129
]
@@ -114,6 +134,12 @@ version = "1.5.0"
134
source = "registry+https://github.com/rust-lang/crates.io-index"
135
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
136
137
+[[package]]
138
+name = "base64"
139
+version = "0.13.1"
140
+source = "registry+https://github.com/rust-lang/crates.io-index"
141
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
142
+
143
[[package]]
144
name = "base64"
145
version = "0.22.1"
@@ -132,6 +158,12 @@ version = "2.11.0"
158
source = "registry+https://github.com/rust-lang/crates.io-index"
159
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
160
161
+[[package]]
162
+name = "block"
163
+version = "0.1.6"
164
+source = "registry+https://github.com/rust-lang/crates.io-index"
165
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
166
+
167
[[package]]
168
name = "block-buffer"
169
version = "0.10.4"
@@ -141,18 +173,116 @@ dependencies = [
173
"generic-array",
174
]
175
176
+[[package]]
177
+name = "block2"
178
+version = "0.6.2"
179
+source = "registry+https://github.com/rust-lang/crates.io-index"
180
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
181
+dependencies = [
182
+ "objc2",
183
+]
184
+
185
[[package]]
186
name = "bumpalo"
187
version = "3.20.2"
188
source = "registry+https://github.com/rust-lang/crates.io-index"
189
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
190
191
+[[package]]
192
+name = "bytemuck"
193
+version = "1.25.0"
194
+source = "registry+https://github.com/rust-lang/crates.io-index"
195
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
196
+dependencies = [
197
+ "bytemuck_derive",
198
+]
199
+
200
+[[package]]
201
+name = "bytemuck_derive"
202
+version = "1.10.2"
203
+source = "registry+https://github.com/rust-lang/crates.io-index"
204
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
205
+dependencies = [
206
+ "proc-macro2",
207
+ "quote",
208
+ "syn 2.0.117",
209
+]
210
+
211
+[[package]]
212
+name = "byteorder"
213
+version = "1.5.0"
214
+source = "registry+https://github.com/rust-lang/crates.io-index"
215
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
216
+
217
[[package]]
218
name = "bytes"
219
version = "1.11.1"
220
source = "registry+https://github.com/rust-lang/crates.io-index"
221
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
222
223
+[[package]]
224
+name = "candle-core"
225
+version = "0.10.2"
226
+dependencies = [
227
+ "accelerate-src",
228
+ "byteorder",
229
+ "candle-metal-kernels",
230
+ "candle-ug",
231
+ "float8",
232
+ "gemm 0.19.0",
233
+ "half",
234
+ "libc",
235
+ "libm",
236
+ "memmap2",
237
+ "num-traits",
238
+ "num_cpus",
239
+ "objc2-foundation",
240
+ "objc2-metal",
241
+ "rand 0.9.2",
242
+ "rand_distr",
243
+ "rayon",
244
+ "safetensors 0.7.0",
245
+ "thiserror 2.0.18",
246
+ "tokenizers 0.22.2",
247
+ "yoke 0.8.2",
248
+ "zip",
249
+]
250
+
251
+[[package]]
252
+name = "candle-metal-kernels"
253
+version = "0.10.2"
254
+dependencies = [
255
+ "half",
256
+ "objc2",
257
+ "objc2-foundation",
258
+ "objc2-metal",
259
+ "once_cell",
260
+ "thiserror 2.0.18",
261
+ "tracing",
262
+]
263
+
264
+[[package]]
265
+name = "candle-nn"
266
+version = "0.10.2"
267
+dependencies = [
268
+ "candle-core",
269
+ "half",
270
+ "libc",
271
+ "num-traits",
272
+ "rayon",
273
+ "safetensors 0.7.0",
274
+ "serde",
275
+ "thiserror 2.0.18",
276
+]
277
+
278
+[[package]]
279
+name = "candle-ug"
280
+version = "0.10.2"
281
+dependencies = [
282
+ "ug",
283
+ "ug-metal",
284
+]
285
+
286
[[package]]
287
name = "cassowary"
288
version = "0.3.0"
@@ -279,6 +409,21 @@ dependencies = [
409
"static_assertions",
410
]
411
412
+[[package]]
413
+name = "compact_str"
414
+version = "0.9.0"
415
+source = "registry+https://github.com/rust-lang/crates.io-index"
416
+checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
417
+dependencies = [
418
+ "castaway",
419
+ "cfg-if",
420
+ "itoa",
421
+ "rustversion",
422
+ "ryu",
423
+ "serde",
424
+ "static_assertions",
425
+]
426
+
427
[[package]]
428
name = "convert_case"
429
version = "0.6.0"
@@ -288,6 +433,16 @@ dependencies = [
433
"unicode-segmentation",
434
]
435
436
+[[package]]
437
+name = "core-foundation"
438
+version = "0.9.4"
439
+source = "registry+https://github.com/rust-lang/crates.io-index"
440
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
441
+dependencies = [
442
+ "core-foundation-sys",
443
+ "libc",
444
+]
445
+
446
[[package]]
447
name = "core-foundation"
448
version = "0.10.1"
@@ -304,6 +459,17 @@ version = "0.8.7"
459
source = "registry+https://github.com/rust-lang/crates.io-index"
460
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
461
462
+[[package]]
463
+name = "core-graphics-types"
464
+version = "0.1.3"
465
+source = "registry+https://github.com/rust-lang/crates.io-index"
466
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
467
+dependencies = [
468
+ "bitflags 1.3.2",
469
+ "core-foundation 0.9.4",
470
+ "libc",
471
+]
472
+
473
[[package]]
474
name = "cpufeatures"
475
version = "0.2.17"
@@ -313,6 +479,40 @@ dependencies = [
479
"libc",
480
]
481
482
+[[package]]
483
+name = "crc32fast"
484
+version = "1.5.0"
485
+source = "registry+https://github.com/rust-lang/crates.io-index"
486
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
487
+dependencies = [
488
+ "cfg-if",
489
+]
490
+
491
+[[package]]
492
+name = "crossbeam-deque"
493
+version = "0.8.6"
494
+source = "registry+https://github.com/rust-lang/crates.io-index"
495
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
496
+dependencies = [
497
+ "crossbeam-epoch",
498
+ "crossbeam-utils",
499
+]
500
+
501
+[[package]]
502
+name = "crossbeam-epoch"
503
+version = "0.9.18"
504
+source = "registry+https://github.com/rust-lang/crates.io-index"
505
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
506
+dependencies = [
507
+ "crossbeam-utils",
508
+]
509
+
510
+[[package]]
511
+name = "crossbeam-utils"
512
+version = "0.8.21"
513
+source = "registry+https://github.com/rust-lang/crates.io-index"
514
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
515
+
516
[[package]]
517
name = "crossterm"
518
version = "0.28.1"
@@ -339,6 +539,12 @@ dependencies = [
539
"winapi",
540
]
541
542
+[[package]]
543
+name = "crunchy"
544
+version = "0.2.4"
545
+source = "registry+https://github.com/rust-lang/crates.io-index"
546
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
547
+
548
[[package]]
549
name = "crypto-common"
550
version = "0.1.7"
@@ -349,14 +555,38 @@ dependencies = [
555
"typenum",
556
]
557
558
+[[package]]
559
+name = "darling"
560
+version = "0.20.11"
561
+source = "registry+https://github.com/rust-lang/crates.io-index"
562
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
563
+dependencies = [
564
+ "darling_core 0.20.11",
565
+ "darling_macro 0.20.11",
566
+]
567
+
568
[[package]]
569
name = "darling"
570
version = "0.23.0"
571
source = "registry+https://github.com/rust-lang/crates.io-index"
572
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
573
dependencies = [
358
- "darling_core",
359
- "darling_macro",
574
+ "darling_core 0.23.0",
575
+ "darling_macro 0.23.0",
576
+]
577
+
578
+[[package]]
579
+name = "darling_core"
580
+version = "0.20.11"
581
+source = "registry+https://github.com/rust-lang/crates.io-index"
582
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
583
+dependencies = [
584
+ "fnv",
585
+ "ident_case",
586
+ "proc-macro2",
587
+ "quote",
588
+ "strsim 0.11.1",
589
+ "syn 2.0.117",
590
]
591
592
[[package]]
@@ -372,17 +602,68 @@ dependencies = [
602
"syn 2.0.117",
603
]
604
605
+[[package]]
606
+name = "darling_macro"
607
+version = "0.20.11"
608
+source = "registry+https://github.com/rust-lang/crates.io-index"
609
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
610
+dependencies = [
611
+ "darling_core 0.20.11",
612
+ "quote",
613
+ "syn 2.0.117",
614
+]
615
+
616
[[package]]
617
name = "darling_macro"
618
version = "0.23.0"
619
source = "registry+https://github.com/rust-lang/crates.io-index"
620
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
621
dependencies = [
381
- "darling_core",
622
+ "darling_core 0.23.0",
623
+ "quote",
624
+ "syn 2.0.117",
625
+]
626
+
627
+[[package]]
628
+name = "dary_heap"
629
+version = "0.3.9"
630
+source = "registry+https://github.com/rust-lang/crates.io-index"
631
+checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe"
632
+dependencies = [
633
+ "serde",
634
+]
635
+
636
+[[package]]
637
+name = "derive_builder"
638
+version = "0.20.2"
639
+source = "registry+https://github.com/rust-lang/crates.io-index"
640
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
641
+dependencies = [
642
+ "derive_builder_macro",
643
+]
644
+
645
+[[package]]
646
+name = "derive_builder_core"
647
+version = "0.20.2"
648
+source = "registry+https://github.com/rust-lang/crates.io-index"
649
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
650
+dependencies = [
651
+ "darling 0.20.11",
652
+ "proc-macro2",
653
"quote",
654
"syn 2.0.117",
655
]
656
657
+[[package]]
658
+name = "derive_builder_macro"
659
+version = "0.20.2"
660
+source = "registry+https://github.com/rust-lang/crates.io-index"
661
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
662
+dependencies = [
663
+ "derive_builder_core",
664
+ "syn 2.0.117",
665
+]
666
+
667
[[package]]
668
name = "digest"
669
version = "0.10.7"
@@ -414,6 +695,16 @@ dependencies = [
695
"windows-sys 0.48.0",
696
]
697
698
+[[package]]
699
+name = "dispatch2"
700
+version = "0.3.1"
701
+source = "registry+https://github.com/rust-lang/crates.io-index"
702
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
703
+dependencies = [
704
+ "bitflags 2.11.0",
705
+ "objc2",
706
+]
707
+
708
[[package]]
709
name = "displaydoc"
710
version = "0.2.5"
@@ -431,12 +722,40 @@ version = "0.15.7"
722
source = "registry+https://github.com/rust-lang/crates.io-index"
723
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
724
725
+[[package]]
726
+name = "dyn-stack"
727
+version = "0.13.2"
728
+source = "registry+https://github.com/rust-lang/crates.io-index"
729
+checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8"
730
+dependencies = [
731
+ "bytemuck",
732
+ "dyn-stack-macros",
733
+]
734
+
735
+[[package]]
736
+name = "dyn-stack-macros"
737
+version = "0.1.3"
738
+source = "registry+https://github.com/rust-lang/crates.io-index"
739
+checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9"
740
+
741
[[package]]
742
name = "either"
743
version = "1.15.0"
744
source = "registry+https://github.com/rust-lang/crates.io-index"
745
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
746
747
+[[package]]
748
+name = "enum-as-inner"
749
+version = "0.6.1"
750
+source = "registry+https://github.com/rust-lang/crates.io-index"
751
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
752
+dependencies = [
753
+ "heck 0.5.0",
754
+ "proc-macro2",
755
+ "quote",
756
+ "syn 2.0.117",
757
+]
758
+
759
[[package]]
760
name = "equivalent"
761
version = "1.0.2"
@@ -453,18 +772,75 @@ dependencies = [
772
"windows-sys 0.61.2",
773
]
774
775
+[[package]]
776
+name = "esaxx-rs"
777
+version = "0.1.10"
778
+source = "registry+https://github.com/rust-lang/crates.io-index"
779
+checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
780
+
781
[[package]]
782
name = "find-msvc-tools"
783
version = "0.1.9"
784
source = "registry+https://github.com/rust-lang/crates.io-index"
785
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
786
787
+[[package]]
788
+name = "float8"
789
+version = "0.7.0"
790
+source = "registry+https://github.com/rust-lang/crates.io-index"
791
+checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc"
792
+dependencies = [
793
+ "half",
794
+ "num-traits",
795
+ "rand 0.9.2",
796
+ "rand_distr",
797
+]
798
+
799
+[[package]]
800
+name = "fnv"
801
+version = "1.0.7"
802
+source = "registry+https://github.com/rust-lang/crates.io-index"
803
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
804
+
805
[[package]]
806
name = "foldhash"
807
version = "0.1.5"
808
source = "registry+https://github.com/rust-lang/crates.io-index"
809
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
810
811
+[[package]]
812
+name = "foldhash"
813
+version = "0.2.0"
814
+source = "registry+https://github.com/rust-lang/crates.io-index"
815
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
816
+
817
+[[package]]
818
+name = "foreign-types"
819
+version = "0.5.0"
820
+source = "registry+https://github.com/rust-lang/crates.io-index"
821
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
822
+dependencies = [
823
+ "foreign-types-macros",
824
+ "foreign-types-shared",
825
+]
826
+
827
+[[package]]
828
+name = "foreign-types-macros"
829
+version = "0.2.3"
830
+source = "registry+https://github.com/rust-lang/crates.io-index"
831
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
832
+dependencies = [
833
+ "proc-macro2",
834
+ "quote",
835
+ "syn 2.0.117",
836
+]
837
+
838
+[[package]]
839
+name = "foreign-types-shared"
840
+version = "0.3.1"
841
+source = "registry+https://github.com/rust-lang/crates.io-index"
842
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
843
+
844
[[package]]
845
name = "form_urlencoded"
846
version = "1.2.2"
@@ -563,33 +939,271 @@ dependencies = [
939
]
940
941
[[package]]
566
-name = "generator"
567
-version = "0.7.5"
942
+name = "gemm"
943
+version = "0.18.2"
944
source = "registry+https://github.com/rust-lang/crates.io-index"
569
-checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
945
+checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451"
946
dependencies = [
571
- "cc",
572
- "libc",
573
- "log",
574
- "rustversion",
575
- "windows",
947
+ "dyn-stack",
948
+ "gemm-c32 0.18.2",
949
+ "gemm-c64 0.18.2",
950
+ "gemm-common 0.18.2",
951
+ "gemm-f16 0.18.2",
952
+ "gemm-f32 0.18.2",
953
+ "gemm-f64 0.18.2",
954
+ "num-complex",
955
+ "num-traits",
956
+ "paste",
957
+ "raw-cpuid",
958
+ "seq-macro",
959
]
960
961
[[package]]
579
-name = "generic-array"
580
-version = "0.14.7"
962
+name = "gemm"
963
+version = "0.19.0"
964
source = "registry+https://github.com/rust-lang/crates.io-index"
582
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
965
+checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb"
966
dependencies = [
584
- "typenum",
585
- "version_check",
967
+ "dyn-stack",
968
+ "gemm-c32 0.19.0",
969
+ "gemm-c64 0.19.0",
970
+ "gemm-common 0.19.0",
971
+ "gemm-f16 0.19.0",
972
+ "gemm-f32 0.19.0",
973
+ "gemm-f64 0.19.0",
974
+ "num-complex",
975
+ "num-traits",
976
+ "paste",
977
+ "raw-cpuid",
978
+ "seq-macro",
979
]
980
981
[[package]]
589
-name = "getrandom"
590
-version = "0.2.17"
982
+name = "gemm-c32"
983
+version = "0.18.2"
984
source = "registry+https://github.com/rust-lang/crates.io-index"
592
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
985
+checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847"
986
+dependencies = [
987
+ "dyn-stack",
988
+ "gemm-common 0.18.2",
989
+ "num-complex",
990
+ "num-traits",
991
+ "paste",
992
+ "raw-cpuid",
993
+ "seq-macro",
994
+]
995
+
996
+[[package]]
997
+name = "gemm-c32"
998
+version = "0.19.0"
999
+source = "registry+https://github.com/rust-lang/crates.io-index"
1000
+checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c"
1001
+dependencies = [
1002
+ "dyn-stack",
1003
+ "gemm-common 0.19.0",
1004
+ "num-complex",
1005
+ "num-traits",
1006
+ "paste",
1007
+ "raw-cpuid",
1008
+ "seq-macro",
1009
+]
1010
+
1011
+[[package]]
1012
+name = "gemm-c64"
1013
+version = "0.18.2"
1014
+source = "registry+https://github.com/rust-lang/crates.io-index"
1015
+checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf"
1016
+dependencies = [
1017
+ "dyn-stack",
1018
+ "gemm-common 0.18.2",
1019
+ "num-complex",
1020
+ "num-traits",
1021
+ "paste",
1022
+ "raw-cpuid",
1023
+ "seq-macro",
1024
+]
1025
+
1026
+[[package]]
1027
+name = "gemm-c64"
1028
+version = "0.19.0"
1029
+source = "registry+https://github.com/rust-lang/crates.io-index"
1030
+checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f"
1031
+dependencies = [
1032
+ "dyn-stack",
1033
+ "gemm-common 0.19.0",
1034
+ "num-complex",
1035
+ "num-traits",
1036
+ "paste",
1037
+ "raw-cpuid",
1038
+ "seq-macro",
1039
+]
1040
+
1041
+[[package]]
1042
+name = "gemm-common"
1043
+version = "0.18.2"
1044
+source = "registry+https://github.com/rust-lang/crates.io-index"
1045
+checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3"
1046
+dependencies = [
1047
+ "bytemuck",
1048
+ "dyn-stack",
1049
+ "half",
1050
+ "libm",
1051
+ "num-complex",
1052
+ "num-traits",
1053
+ "once_cell",
1054
+ "paste",
1055
+ "pulp 0.21.5",
1056
+ "raw-cpuid",
1057
+ "rayon",
1058
+ "seq-macro",
1059
+ "sysctl",
1060
+]
1061
+
1062
+[[package]]
1063
+name = "gemm-common"
1064
+version = "0.19.0"
1065
+source = "registry+https://github.com/rust-lang/crates.io-index"
1066
+checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e"
1067
+dependencies = [
1068
+ "bytemuck",
1069
+ "dyn-stack",
1070
+ "half",
1071
+ "libm",
1072
+ "num-complex",
1073
+ "num-traits",
1074
+ "once_cell",
1075
+ "paste",
1076
+ "pulp 0.22.2",
1077
+ "raw-cpuid",
1078
+ "rayon",
1079
+ "seq-macro",
1080
+ "sysctl",
1081
+]
1082
+
1083
+[[package]]
1084
+name = "gemm-f16"
1085
+version = "0.18.2"
1086
+source = "registry+https://github.com/rust-lang/crates.io-index"
1087
+checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109"
1088
+dependencies = [
1089
+ "dyn-stack",
1090
+ "gemm-common 0.18.2",
1091
+ "gemm-f32 0.18.2",
1092
+ "half",
1093
+ "num-complex",
1094
+ "num-traits",
1095
+ "paste",
1096
+ "raw-cpuid",
1097
+ "rayon",
1098
+ "seq-macro",
1099
+]
1100
+
1101
+[[package]]
1102
+name = "gemm-f16"
1103
+version = "0.19.0"
1104
+source = "registry+https://github.com/rust-lang/crates.io-index"
1105
+checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e"
1106
+dependencies = [
1107
+ "dyn-stack",
1108
+ "gemm-common 0.19.0",
1109
+ "gemm-f32 0.19.0",
1110
+ "half",
1111
+ "num-complex",
1112
+ "num-traits",
1113
+ "paste",
1114
+ "raw-cpuid",
1115
+ "rayon",
1116
+ "seq-macro",
1117
+]
1118
+
1119
+[[package]]
1120
+name = "gemm-f32"
1121
+version = "0.18.2"
1122
+source = "registry+https://github.com/rust-lang/crates.io-index"
1123
+checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864"
1124
+dependencies = [
1125
+ "dyn-stack",
1126
+ "gemm-common 0.18.2",
1127
+ "num-complex",
1128
+ "num-traits",
1129
+ "paste",
1130
+ "raw-cpuid",
1131
+ "seq-macro",
1132
+]
1133
+
1134
+[[package]]
1135
+name = "gemm-f32"
1136
+version = "0.19.0"
1137
+source = "registry+https://github.com/rust-lang/crates.io-index"
1138
+checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c"
1139
+dependencies = [
1140
+ "dyn-stack",
1141
+ "gemm-common 0.19.0",
1142
+ "num-complex",
1143
+ "num-traits",
1144
+ "paste",
1145
+ "raw-cpuid",
1146
+ "seq-macro",
1147
+]
1148
+
1149
+[[package]]
1150
+name = "gemm-f64"
1151
+version = "0.18.2"
1152
+source = "registry+https://github.com/rust-lang/crates.io-index"
1153
+checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd"
1154
+dependencies = [
1155
+ "dyn-stack",
1156
+ "gemm-common 0.18.2",
1157
+ "num-complex",
1158
+ "num-traits",
1159
+ "paste",
1160
+ "raw-cpuid",
1161
+ "seq-macro",
1162
+]
1163
+
1164
+[[package]]
1165
+name = "gemm-f64"
1166
+version = "0.19.0"
1167
+source = "registry+https://github.com/rust-lang/crates.io-index"
1168
+checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a"
1169
+dependencies = [
1170
+ "dyn-stack",
1171
+ "gemm-common 0.19.0",
1172
+ "num-complex",
1173
+ "num-traits",
1174
+ "paste",
1175
+ "raw-cpuid",
1176
+ "seq-macro",
1177
+]
1178
+
1179
+[[package]]
1180
+name = "generator"
1181
+version = "0.7.5"
1182
+source = "registry+https://github.com/rust-lang/crates.io-index"
1183
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
1184
+dependencies = [
1185
+ "cc",
1186
+ "libc",
1187
+ "log",
1188
+ "rustversion",
1189
+ "windows",
1190
+]
1191
+
1192
+[[package]]
1193
+name = "generic-array"
1194
+version = "0.14.7"
1195
+source = "registry+https://github.com/rust-lang/crates.io-index"
1196
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
1197
+dependencies = [
1198
+ "typenum",
1199
+ "version_check",
1200
+]
1201
+
1202
+[[package]]
1203
+name = "getrandom"
1204
+version = "0.2.17"
1205
+source = "registry+https://github.com/rust-lang/crates.io-index"
1206
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
1207
dependencies = [
1208
"cfg-if",
1209
"js-sys",
@@ -625,6 +1239,21 @@ dependencies = [
1239
"wasip3",
1240
]
1241
1242
+[[package]]
1243
+name = "half"
1244
+version = "2.7.1"
1245
+source = "registry+https://github.com/rust-lang/crates.io-index"
1246
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
1247
+dependencies = [
1248
+ "bytemuck",
1249
+ "cfg-if",
1250
+ "crunchy",
1251
+ "num-traits",
1252
+ "rand 0.9.2",
1253
+ "rand_distr",
1254
+ "zerocopy",
1255
+]
1256
+
1257
[[package]]
1258
name = "hashbrown"
1259
version = "0.15.5"
@@ -633,7 +1262,20 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1262
dependencies = [
1263
"allocator-api2",
1264
"equivalent",
636
- "foldhash",
1265
+ "foldhash 0.1.5",
1266
+]
1267
+
1268
+[[package]]
1269
+name = "hashbrown"
1270
+version = "0.16.1"
1271
+source = "registry+https://github.com/rust-lang/crates.io-index"
1272
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1273
+dependencies = [
1274
+ "allocator-api2",
1275
+ "equivalent",
1276
+ "foldhash 0.2.0",
1277
+ "serde",
1278
+ "serde_core",
1279
]
1280
1281
[[package]]
@@ -672,6 +1314,12 @@ dependencies = [
1314
"libc",
1315
]
1316
1317
+[[package]]
1318
+name = "hermit-abi"
1319
+version = "0.5.2"
1320
+source = "registry+https://github.com/rust-lang/crates.io-index"
1321
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
1322
+
1323
[[package]]
1324
name = "http"
1325
version = "1.4.0"
@@ -755,7 +1403,7 @@ version = "0.1.20"
1403
source = "registry+https://github.com/rust-lang/crates.io-index"
1404
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
1405
dependencies = [
758
- "base64",
1406
+ "base64 0.22.1",
1407
"bytes",
1408
"futures-channel",
1409
"futures-util",
@@ -805,7 +1453,7 @@ dependencies = [
1453
"displaydoc",
1454
"potential_utf",
1455
"utf8_iter",
808
- "yoke",
1456
+ "yoke 0.8.2",
1457
"zerofrom",
1458
"zerovec",
1459
]
@@ -872,7 +1520,7 @@ dependencies = [
1520
"displaydoc",
1521
"icu_locale_core",
1522
"writeable",
875
- "yoke",
1523
+ "yoke 0.8.2",
1524
"zerofrom",
1525
"zerotrie",
1526
"zerovec",
@@ -938,7 +1586,7 @@ version = "0.3.12"
1586
source = "registry+https://github.com/rust-lang/crates.io-index"
1587
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
1588
dependencies = [
941
- "darling",
1589
+ "darling 0.23.0",
1590
"indoc",
1591
"proc-macro2",
1592
"quote",
@@ -967,6 +1615,24 @@ version = "1.70.2"
1615
source = "registry+https://github.com/rust-lang/crates.io-index"
1616
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
1617
1618
+[[package]]
1619
+name = "itertools"
1620
+version = "0.11.0"
1621
+source = "registry+https://github.com/rust-lang/crates.io-index"
1622
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
1623
+dependencies = [
1624
+ "either",
1625
+]
1626
+
1627
+[[package]]
1628
+name = "itertools"
1629
+version = "0.12.1"
1630
+source = "registry+https://github.com/rust-lang/crates.io-index"
1631
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
1632
+dependencies = [
1633
+ "either",
1634
+]
1635
+
1636
[[package]]
1637
name = "itertools"
1638
version = "0.13.0"
@@ -976,6 +1642,15 @@ dependencies = [
1642
"either",
1643
]
1644
1645
+[[package]]
1646
+name = "itertools"
1647
+version = "0.14.0"
1648
+source = "registry+https://github.com/rust-lang/crates.io-index"
1649
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
1650
+dependencies = [
1651
+ "either",
1652
+]
1653
+
1654
[[package]]
1655
name = "itoa"
1656
version = "1.0.18"
@@ -1012,6 +1687,22 @@ version = "0.2.184"
1687
source = "registry+https://github.com/rust-lang/crates.io-index"
1688
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
1689
1690
+[[package]]
1691
+name = "libloading"
1692
+version = "0.8.9"
1693
+source = "registry+https://github.com/rust-lang/crates.io-index"
1694
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
1695
+dependencies = [
1696
+ "cfg-if",
1697
+ "windows-link",
1698
+]
1699
+
1700
+[[package]]
1701
+name = "libm"
1702
+version = "0.2.16"
1703
+source = "registry+https://github.com/rust-lang/crates.io-index"
1704
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
1705
+
1706
[[package]]
1707
name = "libredox"
1708
version = "0.1.16"
@@ -1078,6 +1769,31 @@ version = "0.1.2"
1769
source = "registry+https://github.com/rust-lang/crates.io-index"
1770
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
1771
1772
+[[package]]
1773
+name = "macro_rules_attribute"
1774
+version = "0.2.2"
1775
+source = "registry+https://github.com/rust-lang/crates.io-index"
1776
+checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
1777
+dependencies = [
1778
+ "macro_rules_attribute-proc_macro",
1779
+ "paste",
1780
+]
1781
+
1782
+[[package]]
1783
+name = "macro_rules_attribute-proc_macro"
1784
+version = "0.2.2"
1785
+source = "registry+https://github.com/rust-lang/crates.io-index"
1786
+checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
1787
+
1788
+[[package]]
1789
+name = "malloc_buf"
1790
+version = "0.0.6"
1791
+source = "registry+https://github.com/rust-lang/crates.io-index"
1792
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
1793
+dependencies = [
1794
+ "libc",
1795
+]
1796
+
1797
[[package]]
1798
name = "maplit"
1799
version = "1.0.2"
@@ -1100,33 +1816,234 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1816
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
1817
1818
[[package]]
1103
-name = "mio"
1104
-version = "1.2.0"
1819
+name = "memmap2"
1820
+version = "0.9.10"
1821
+source = "registry+https://github.com/rust-lang/crates.io-index"
1822
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
1823
+dependencies = [
1824
+ "libc",
1825
+ "stable_deref_trait",
1826
+]
1827
+
1828
+[[package]]
1829
+name = "metal"
1830
+version = "0.29.0"
1831
+source = "registry+https://github.com/rust-lang/crates.io-index"
1832
+checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
1833
+dependencies = [
1834
+ "bitflags 2.11.0",
1835
+ "block",
1836
+ "core-graphics-types",
1837
+ "foreign-types",
1838
+ "log",
1839
+ "objc",
1840
+ "paste",
1841
+]
1842
+
1843
+[[package]]
1844
+name = "minimal-lexical"
1845
+version = "0.2.1"
1846
+source = "registry+https://github.com/rust-lang/crates.io-index"
1847
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1848
+
1849
+[[package]]
1850
+name = "mio"
1851
+version = "1.2.0"
1852
+source = "registry+https://github.com/rust-lang/crates.io-index"
1853
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
1854
+dependencies = [
1855
+ "libc",
1856
+ "log",
1857
+ "wasi",
1858
+ "windows-sys 0.61.2",
1859
+]
1860
+
1861
+[[package]]
1862
+name = "monostate"
1863
+version = "0.1.18"
1864
+source = "registry+https://github.com/rust-lang/crates.io-index"
1865
+checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
1866
+dependencies = [
1867
+ "monostate-impl",
1868
+ "serde",
1869
+ "serde_core",
1870
+]
1871
+
1872
+[[package]]
1873
+name = "monostate-impl"
1874
+version = "0.1.18"
1875
+source = "registry+https://github.com/rust-lang/crates.io-index"
1876
+checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
1877
+dependencies = [
1878
+ "proc-macro2",
1879
+ "quote",
1880
+ "syn 2.0.117",
1881
+]
1882
+
1883
+[[package]]
1884
+name = "nom"
1885
+version = "7.1.3"
1886
+source = "registry+https://github.com/rust-lang/crates.io-index"
1887
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1888
+dependencies = [
1889
+ "memchr",
1890
+ "minimal-lexical",
1891
+]
1892
+
1893
+[[package]]
1894
+name = "nu-ansi-term"
1895
+version = "0.50.3"
1896
+source = "registry+https://github.com/rust-lang/crates.io-index"
1897
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
1898
+dependencies = [
1899
+ "windows-sys 0.61.2",
1900
+]
1901
+
1902
+[[package]]
1903
+name = "num"
1904
+version = "0.4.3"
1905
+source = "registry+https://github.com/rust-lang/crates.io-index"
1906
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
1907
+dependencies = [
1908
+ "num-bigint",
1909
+ "num-complex",
1910
+ "num-integer",
1911
+ "num-iter",
1912
+ "num-rational",
1913
+ "num-traits",
1914
+]
1915
+
1916
+[[package]]
1917
+name = "num-bigint"
1918
+version = "0.4.6"
1919
+source = "registry+https://github.com/rust-lang/crates.io-index"
1920
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
1921
+dependencies = [
1922
+ "num-integer",
1923
+ "num-traits",
1924
+]
1925
+
1926
+[[package]]
1927
+name = "num-complex"
1928
+version = "0.4.6"
1929
+source = "registry+https://github.com/rust-lang/crates.io-index"
1930
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
1931
+dependencies = [
1932
+ "bytemuck",
1933
+ "num-traits",
1934
+]
1935
+
1936
+[[package]]
1937
+name = "num-integer"
1938
+version = "0.1.46"
1939
+source = "registry+https://github.com/rust-lang/crates.io-index"
1940
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1941
+dependencies = [
1942
+ "num-traits",
1943
+]
1944
+
1945
+[[package]]
1946
+name = "num-iter"
1947
+version = "0.1.45"
1948
+source = "registry+https://github.com/rust-lang/crates.io-index"
1949
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
1950
+dependencies = [
1951
+ "autocfg",
1952
+ "num-integer",
1953
+ "num-traits",
1954
+]
1955
+
1956
+[[package]]
1957
+name = "num-rational"
1958
+version = "0.4.2"
1959
+source = "registry+https://github.com/rust-lang/crates.io-index"
1960
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
1961
+dependencies = [
1962
+ "num-bigint",
1963
+ "num-integer",
1964
+ "num-traits",
1965
+]
1966
+
1967
+[[package]]
1968
+name = "num-traits"
1969
+version = "0.2.19"
1970
+source = "registry+https://github.com/rust-lang/crates.io-index"
1971
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1972
+dependencies = [
1973
+ "autocfg",
1974
+ "libm",
1975
+]
1976
+
1977
+[[package]]
1978
+name = "num_cpus"
1979
+version = "1.17.0"
1980
+source = "registry+https://github.com/rust-lang/crates.io-index"
1981
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
1982
+dependencies = [
1983
+ "hermit-abi 0.5.2",
1984
+ "libc",
1985
+]
1986
+
1987
+[[package]]
1988
+name = "objc"
1989
+version = "0.2.7"
1990
+source = "registry+https://github.com/rust-lang/crates.io-index"
1991
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
1992
+dependencies = [
1993
+ "malloc_buf",
1994
+]
1995
+
1996
+[[package]]
1997
+name = "objc2"
1998
+version = "0.6.4"
1999
+source = "registry+https://github.com/rust-lang/crates.io-index"
2000
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
2001
+dependencies = [
2002
+ "objc2-encode",
2003
+]
2004
+
2005
+[[package]]
2006
+name = "objc2-core-foundation"
2007
+version = "0.3.2"
2008
source = "registry+https://github.com/rust-lang/crates.io-index"
1106
-checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
2009
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
2010
dependencies = [
1108
- "libc",
1109
- "log",
1110
- "wasi",
1111
- "windows-sys 0.61.2",
2011
+ "bitflags 2.11.0",
2012
+ "dispatch2",
2013
+ "objc2",
2014
]
2015
2016
[[package]]
1115
-name = "nu-ansi-term"
1116
-version = "0.50.3"
2017
+name = "objc2-encode"
2018
+version = "4.1.0"
2019
source = "registry+https://github.com/rust-lang/crates.io-index"
1118
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
2020
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
2021
+
2022
+[[package]]
2023
+name = "objc2-foundation"
2024
+version = "0.3.2"
2025
+source = "registry+https://github.com/rust-lang/crates.io-index"
2026
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
2027
dependencies = [
1120
- "windows-sys 0.61.2",
2028
+ "bitflags 2.11.0",
2029
+ "block2",
2030
+ "libc",
2031
+ "objc2",
2032
+ "objc2-core-foundation",
2033
]
2034
2035
[[package]]
1124
-name = "num-traits"
1125
-version = "0.2.19"
2036
+name = "objc2-metal"
2037
+version = "0.3.2"
2038
source = "registry+https://github.com/rust-lang/crates.io-index"
1127
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
2039
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
2040
dependencies = [
1129
- "autocfg",
2041
+ "bitflags 2.11.0",
2042
+ "block2",
2043
+ "dispatch2",
2044
+ "objc2",
2045
+ "objc2-core-foundation",
2046
+ "objc2-foundation",
2047
]
2048
2049
[[package]]
@@ -1146,6 +2063,8 @@ name = "onde-cli"
2063
version = "0.1.1"
2064
dependencies = [
2065
"anyhow",
2066
+ "candle-core",
2067
+ "candle-nn",
2068
"crossterm",
2069
"dirs",
2070
"dotenvy",
@@ -1153,13 +2072,38 @@ dependencies = [
2072
"libc",
2073
"log",
2074
"ratatui",
2075
+ "serde",
2076
+ "serde_json",
2077
"smbcloud-auth-sdk",
2078
"smbcloud-gresiq-sdk",
2079
"smbcloud-model",
2080
"smbcloud-network",
2081
+ "tokenizers 0.20.4",
2082
"tokio",
2083
]
2084
2085
+[[package]]
2086
+name = "onig"
2087
+version = "6.5.1"
2088
+source = "registry+https://github.com/rust-lang/crates.io-index"
2089
+checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
2090
+dependencies = [
2091
+ "bitflags 2.11.0",
2092
+ "libc",
2093
+ "once_cell",
2094
+ "onig_sys",
2095
+]
2096
+
2097
+[[package]]
2098
+name = "onig_sys"
2099
+version = "69.9.1"
2100
+source = "registry+https://github.com/rust-lang/crates.io-index"
2101
+checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
2102
+dependencies = [
2103
+ "cc",
2104
+ "pkg-config",
2105
+]
2106
+
2107
[[package]]
2108
name = "openssl-probe"
2109
version = "0.2.1"
@@ -1213,6 +2157,12 @@ version = "0.2.17"
2157
source = "registry+https://github.com/rust-lang/crates.io-index"
2158
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
2159
2160
+[[package]]
2161
+name = "pkg-config"
2162
+version = "0.3.33"
2163
+source = "registry+https://github.com/rust-lang/crates.io-index"
2164
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
2165
+
2166
[[package]]
2167
name = "potential_utf"
2168
version = "0.1.5"
@@ -1274,6 +2224,43 @@ dependencies = [
2224
"unicode-ident",
2225
]
2226
2227
+[[package]]
2228
+name = "pulp"
2229
+version = "0.21.5"
2230
+source = "registry+https://github.com/rust-lang/crates.io-index"
2231
+checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907"
2232
+dependencies = [
2233
+ "bytemuck",
2234
+ "cfg-if",
2235
+ "libm",
2236
+ "num-complex",
2237
+ "reborrow",
2238
+ "version_check",
2239
+]
2240
+
2241
+[[package]]
2242
+name = "pulp"
2243
+version = "0.22.2"
2244
+source = "registry+https://github.com/rust-lang/crates.io-index"
2245
+checksum = "2e205bb30d5b916c55e584c22201771bcf2bad9aabd5d4127f38387140c38632"
2246
+dependencies = [
2247
+ "bytemuck",
2248
+ "cfg-if",
2249
+ "libm",
2250
+ "num-complex",
2251
+ "paste",
2252
+ "pulp-wasm-simd-flag",
2253
+ "raw-cpuid",
2254
+ "reborrow",
2255
+ "version_check",
2256
+]
2257
+
2258
+[[package]]
2259
+name = "pulp-wasm-simd-flag"
2260
+version = "0.1.0"
2261
+source = "registry+https://github.com/rust-lang/crates.io-index"
2262
+checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0"
2263
+
2264
[[package]]
2265
name = "quinn"
2266
version = "0.11.9"
@@ -1303,7 +2290,7 @@ dependencies = [
2290
"bytes",
2291
"getrandom 0.3.4",
2292
"lru-slab",
1306
- "rand",
2293
+ "rand 0.9.2",
2294
"ring",
2295
"rustc-hash",
2296
"rustls",
@@ -1350,14 +2337,35 @@ version = "6.0.0"
2337
source = "registry+https://github.com/rust-lang/crates.io-index"
2338
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
2339
2340
+[[package]]
2341
+name = "rand"
2342
+version = "0.8.6"
2343
+source = "registry+https://github.com/rust-lang/crates.io-index"
2344
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
2345
+dependencies = [
2346
+ "libc",
2347
+ "rand_chacha 0.3.1",
2348
+ "rand_core 0.6.4",
2349
+]
2350
+
2351
[[package]]
2352
name = "rand"
2353
version = "0.9.2"
2354
source = "registry+https://github.com/rust-lang/crates.io-index"
2355
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
2356
dependencies = [
1359
- "rand_chacha",
1360
- "rand_core",
2357
+ "rand_chacha 0.9.0",
2358
+ "rand_core 0.9.5",
2359
+]
2360
+
2361
+[[package]]
2362
+name = "rand_chacha"
2363
+version = "0.3.1"
2364
+source = "registry+https://github.com/rust-lang/crates.io-index"
2365
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
2366
+dependencies = [
2367
+ "ppv-lite86",
2368
+ "rand_core 0.6.4",
2369
]
2370
2371
[[package]]
@@ -1367,7 +2375,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2375
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
2376
dependencies = [
2377
"ppv-lite86",
1370
- "rand_core",
2378
+ "rand_core 0.9.5",
2379
+]
2380
+
2381
+[[package]]
2382
+name = "rand_core"
2383
+version = "0.6.4"
2384
+source = "registry+https://github.com/rust-lang/crates.io-index"
2385
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
2386
+dependencies = [
2387
+ "getrandom 0.2.17",
2388
]
2389
2390
[[package]]
@@ -1379,6 +2396,16 @@ dependencies = [
2396
"getrandom 0.3.4",
2397
]
2398
2399
+[[package]]
2400
+name = "rand_distr"
2401
+version = "0.5.1"
2402
+source = "registry+https://github.com/rust-lang/crates.io-index"
2403
+checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
2404
+dependencies = [
2405
+ "num-traits",
2406
+ "rand 0.9.2",
2407
+]
2408
+
2409
[[package]]
2410
name = "ratatui"
2411
version = "0.29.0"
@@ -1387,11 +2414,11 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
2414
dependencies = [
2415
"bitflags 2.11.0",
2416
"cassowary",
1390
- "compact_str",
2417
+ "compact_str 0.8.1",
2418
"crossterm",
2419
"indoc",
2420
"instability",
1394
- "itertools",
2421
+ "itertools 0.13.0",
2422
"lru",
2423
"paste",
2424
"strum 0.26.3",
@@ -1400,6 +2427,63 @@ dependencies = [
2427
"unicode-width 0.2.0",
2428
]
2429
2430
+[[package]]
2431
+name = "raw-cpuid"
2432
+version = "11.6.0"
2433
+source = "registry+https://github.com/rust-lang/crates.io-index"
2434
+checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
2435
+dependencies = [
2436
+ "bitflags 2.11.0",
2437
+]
2438
+
2439
+[[package]]
2440
+name = "rayon"
2441
+version = "1.12.0"
2442
+source = "registry+https://github.com/rust-lang/crates.io-index"
2443
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
2444
+dependencies = [
2445
+ "either",
2446
+ "rayon-core",
2447
+]
2448
+
2449
+[[package]]
2450
+name = "rayon-cond"
2451
+version = "0.3.0"
2452
+source = "registry+https://github.com/rust-lang/crates.io-index"
2453
+checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9"
2454
+dependencies = [
2455
+ "either",
2456
+ "itertools 0.11.0",
2457
+ "rayon",
2458
+]
2459
+
2460
+[[package]]
2461
+name = "rayon-cond"
2462
+version = "0.4.0"
2463
+source = "registry+https://github.com/rust-lang/crates.io-index"
2464
+checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f"
2465
+dependencies = [
2466
+ "either",
2467
+ "itertools 0.14.0",
2468
+ "rayon",
2469
+]
2470
+
2471
+[[package]]
2472
+name = "rayon-core"
2473
+version = "1.13.0"
2474
+source = "registry+https://github.com/rust-lang/crates.io-index"
2475
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
2476
+dependencies = [
2477
+ "crossbeam-deque",
2478
+ "crossbeam-utils",
2479
+]
2480
+
2481
+[[package]]
2482
+name = "reborrow"
2483
+version = "0.5.5"
2484
+source = "registry+https://github.com/rust-lang/crates.io-index"
2485
+checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430"
2486
+
2487
[[package]]
2488
name = "redox_syscall"
2489
version = "0.5.18"
@@ -1420,6 +2504,18 @@ dependencies = [
2504
"thiserror 1.0.69",
2505
]
2506
2507
+[[package]]
2508
+name = "regex"
2509
+version = "1.12.3"
2510
+source = "registry+https://github.com/rust-lang/crates.io-index"
2511
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
2512
+dependencies = [
2513
+ "aho-corasick",
2514
+ "memchr",
2515
+ "regex-automata",
2516
+ "regex-syntax",
2517
+]
2518
+
2519
[[package]]
2520
name = "regex-automata"
2521
version = "0.4.14"
@@ -1443,7 +2539,7 @@ version = "0.12.28"
2539
source = "registry+https://github.com/rust-lang/crates.io-index"
2540
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
2541
dependencies = [
1446
- "base64",
2542
+ "base64 0.22.1",
2543
"bytes",
2544
"futures-core",
2545
"http",
@@ -1568,6 +2664,27 @@ version = "1.0.23"
2664
source = "registry+https://github.com/rust-lang/crates.io-index"
2665
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
2666
2667
+[[package]]
2668
+name = "safetensors"
2669
+version = "0.4.5"
2670
+source = "registry+https://github.com/rust-lang/crates.io-index"
2671
+checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6"
2672
+dependencies = [
2673
+ "serde",
2674
+ "serde_json",
2675
+]
2676
+
2677
+[[package]]
2678
+name = "safetensors"
2679
+version = "0.7.0"
2680
+source = "registry+https://github.com/rust-lang/crates.io-index"
2681
+checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5"
2682
+dependencies = [
2683
+ "hashbrown 0.16.1",
2684
+ "serde",
2685
+ "serde_json",
2686
+]
2687
+
2688
[[package]]
2689
name = "same-file"
2690
version = "1.0.6"
@@ -1605,7 +2722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2722
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
2723
dependencies = [
2724
"bitflags 2.11.0",
1608
- "core-foundation",
2725
+ "core-foundation 0.10.1",
2726
"core-foundation-sys",
2727
"libc",
2728
"security-framework-sys",
@@ -1627,6 +2744,12 @@ version = "1.0.28"
2744
source = "registry+https://github.com/rust-lang/crates.io-index"
2745
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
2746
2747
+[[package]]
2748
+name = "seq-macro"
2749
+version = "0.3.6"
2750
+source = "registry+https://github.com/rust-lang/crates.io-index"
2751
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
2752
+
2753
[[package]]
2754
name = "serde"
2755
version = "1.0.228"
@@ -1766,7 +2889,7 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
2889
name = "smbcloud-auth-sdk"
2890
version = "0.3.34"
2891
dependencies = [
1769
- "base64",
2892
+ "base64 0.22.1",
2893
"reqwest",
2894
"serde",
2895
"serde_json",
@@ -1841,6 +2964,18 @@ dependencies = [
2964
"strum 0.24.1",
2965
]
2966
2967
+[[package]]
2968
+name = "spm_precompiled"
2969
+version = "0.1.4"
2970
+source = "registry+https://github.com/rust-lang/crates.io-index"
2971
+checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
2972
+dependencies = [
2973
+ "base64 0.13.1",
2974
+ "nom",
2975
+ "serde",
2976
+ "unicode-segmentation",
2977
+]
2978
+
2979
[[package]]
2980
name = "stable_deref_trait"
2981
version = "1.2.1"
@@ -2008,6 +3143,20 @@ dependencies = [
3143
"syn 2.0.117",
3144
]
3145
3146
+[[package]]
3147
+name = "sysctl"
3148
+version = "0.6.0"
3149
+source = "registry+https://github.com/rust-lang/crates.io-index"
3150
+checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
3151
+dependencies = [
3152
+ "bitflags 2.11.0",
3153
+ "byteorder",
3154
+ "enum-as-inner",
3155
+ "libc",
3156
+ "thiserror 1.0.69",
3157
+ "walkdir",
3158
+]
3159
+
3160
[[package]]
3161
name = "textwrap"
3162
version = "0.11.0"
@@ -2091,6 +3240,70 @@ version = "0.1.1"
3240
source = "registry+https://github.com/rust-lang/crates.io-index"
3241
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
3242
3243
+[[package]]
3244
+name = "tokenizers"
3245
+version = "0.20.4"
3246
+source = "registry+https://github.com/rust-lang/crates.io-index"
3247
+checksum = "3b08cc37428a476fc9e20ac850132a513a2e1ce32b6a31addf2b74fa7033b905"
3248
+dependencies = [
3249
+ "aho-corasick",
3250
+ "derive_builder",
3251
+ "esaxx-rs",
3252
+ "getrandom 0.2.17",
3253
+ "itertools 0.12.1",
3254
+ "lazy_static",
3255
+ "log",
3256
+ "macro_rules_attribute",
3257
+ "monostate",
3258
+ "onig",
3259
+ "paste",
3260
+ "rand 0.8.6",
3261
+ "rayon",
3262
+ "rayon-cond 0.3.0",
3263
+ "regex",
3264
+ "regex-syntax",
3265
+ "serde",
3266
+ "serde_json",
3267
+ "spm_precompiled",
3268
+ "thiserror 1.0.69",
3269
+ "unicode-normalization-alignments",
3270
+ "unicode-segmentation",
3271
+ "unicode_categories",
3272
+]
3273
+
3274
+[[package]]
3275
+name = "tokenizers"
3276
+version = "0.22.2"
3277
+source = "registry+https://github.com/rust-lang/crates.io-index"
3278
+checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223"
3279
+dependencies = [
3280
+ "ahash",
3281
+ "aho-corasick",
3282
+ "compact_str 0.9.0",
3283
+ "dary_heap",
3284
+ "derive_builder",
3285
+ "esaxx-rs",
3286
+ "getrandom 0.3.4",
3287
+ "itertools 0.14.0",
3288
+ "log",
3289
+ "macro_rules_attribute",
3290
+ "monostate",
3291
+ "onig",
3292
+ "paste",
3293
+ "rand 0.9.2",
3294
+ "rayon",
3295
+ "rayon-cond 0.4.0",
3296
+ "regex",
3297
+ "regex-syntax",
3298
+ "serde",
3299
+ "serde_json",
3300
+ "spm_precompiled",
3301
+ "thiserror 2.0.18",
3302
+ "unicode-normalization-alignments",
3303
+ "unicode-segmentation",
3304
+ "unicode_categories",
3305
+]
3306
+
3307
[[package]]
3308
name = "tokio"
3309
version = "1.51.1"
@@ -2261,18 +3474,68 @@ version = "0.1.0"
3474
source = "registry+https://github.com/rust-lang/crates.io-index"
3475
checksum = "8acc9e35a3072d9ea375093f321b3738ea3ac4fdebc18d2789b01581e5c2e4ae"
3476
3477
+[[package]]
3478
+name = "typed-path"
3479
+version = "0.12.3"
3480
+source = "registry+https://github.com/rust-lang/crates.io-index"
3481
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
3482
+
3483
[[package]]
3484
name = "typenum"
3485
version = "1.19.0"
3486
source = "registry+https://github.com/rust-lang/crates.io-index"
3487
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
3488
3489
+[[package]]
3490
+name = "ug"
3491
+version = "0.5.0"
3492
+source = "registry+https://github.com/rust-lang/crates.io-index"
3493
+checksum = "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0"
3494
+dependencies = [
3495
+ "gemm 0.18.2",
3496
+ "half",
3497
+ "libloading",
3498
+ "memmap2",
3499
+ "num",
3500
+ "num-traits",
3501
+ "num_cpus",
3502
+ "rayon",
3503
+ "safetensors 0.4.5",
3504
+ "serde",
3505
+ "thiserror 1.0.69",
3506
+ "tracing",
3507
+ "yoke 0.7.5",
3508
+]
3509
+
3510
+[[package]]
3511
+name = "ug-metal"
3512
+version = "0.5.0"
3513
+source = "registry+https://github.com/rust-lang/crates.io-index"
3514
+checksum = "9f7adf545a99a086d362efc739e7cf4317c18cbeda22706000fd434d70ea3d95"
3515
+dependencies = [
3516
+ "half",
3517
+ "metal",
3518
+ "objc",
3519
+ "serde",
3520
+ "thiserror 1.0.69",
3521
+ "ug",
3522
+]
3523
+
3524
[[package]]
3525
name = "unicode-ident"
3526
version = "1.0.24"
3527
source = "registry+https://github.com/rust-lang/crates.io-index"
3528
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
3529
3530
+[[package]]
3531
+name = "unicode-normalization-alignments"
3532
+version = "0.1.12"
3533
+source = "registry+https://github.com/rust-lang/crates.io-index"
3534
+checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
3535
+dependencies = [
3536
+ "smallvec",
3537
+]
3538
+
3539
[[package]]
3540
name = "unicode-segmentation"
3541
version = "1.13.2"
@@ -2285,7 +3548,7 @@ version = "1.1.0"
3548
source = "registry+https://github.com/rust-lang/crates.io-index"
3549
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
3550
dependencies = [
2288
- "itertools",
3551
+ "itertools 0.13.0",
3552
"unicode-segmentation",
3553
"unicode-width 0.1.14",
3554
]
@@ -2308,6 +3571,12 @@ version = "0.2.6"
3571
source = "registry+https://github.com/rust-lang/crates.io-index"
3572
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
3573
3574
+[[package]]
3575
+name = "unicode_categories"
3576
+version = "0.1.1"
3577
+source = "registry+https://github.com/rust-lang/crates.io-index"
3578
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
3579
+
3580
[[package]]
3581
name = "untrusted"
3582
version = "0.9.0"
@@ -2958,6 +4227,18 @@ version = "0.6.3"
4227
source = "registry+https://github.com/rust-lang/crates.io-index"
4228
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
4229
4230
+[[package]]
4231
+name = "yoke"
4232
+version = "0.7.5"
4233
+source = "registry+https://github.com/rust-lang/crates.io-index"
4234
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
4235
+dependencies = [
4236
+ "serde",
4237
+ "stable_deref_trait",
4238
+ "yoke-derive 0.7.5",
4239
+ "zerofrom",
4240
+]
4241
+
4242
[[package]]
4243
name = "yoke"
4244
version = "0.8.2"
@@ -2965,10 +4246,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4246
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
4247
dependencies = [
4248
"stable_deref_trait",
2968
- "yoke-derive",
4249
+ "yoke-derive 0.8.2",
4250
"zerofrom",
4251
]
4252
4253
+[[package]]
4254
+name = "yoke-derive"
4255
+version = "0.7.5"
4256
+source = "registry+https://github.com/rust-lang/crates.io-index"
4257
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
4258
+dependencies = [
4259
+ "proc-macro2",
4260
+ "quote",
4261
+ "syn 2.0.117",
4262
+ "synstructure",
4263
+]
4264
+
4265
[[package]]
4266
name = "yoke-derive"
4267
version = "0.8.2"
@@ -3035,7 +4328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4328
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
4329
dependencies = [
4330
"displaydoc",
3038
- "yoke",
4331
+ "yoke 0.8.2",
4332
"zerofrom",
4333
]
4334
@@ -3045,7 +4338,7 @@ version = "0.11.6"
4338
source = "registry+https://github.com/rust-lang/crates.io-index"
4339
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
4340
dependencies = [
3048
- "yoke",
4341
+ "yoke 0.8.2",
4342
"zerofrom",
4343
"zerovec-derive",
4344
]
@@ -3061,6 +4354,18 @@ dependencies = [
4354
"syn 2.0.117",
4355
]
4356
4357
+[[package]]
4358
+name = "zip"
4359
+version = "7.2.0"
4360
+source = "registry+https://github.com/rust-lang/crates.io-index"
4361
+checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0"
4362
+dependencies = [
4363
+ "crc32fast",
4364
+ "indexmap",
4365
+ "memchr",
4366
+ "typed-path",
4367
+]
4368
+
4369
[[package]]
4370
name = "zmij"
4371
version = "1.0.21"
Cargo.toml
+6
@@ -33,6 +33,12 @@ smbcloud-model = { path = "../smbcloud-cli/crates/smbcloud-model" }
33
smbcloud-network = { path = "../smbcloud-cli/crates/smbcloud-network" }
34
smbcloud-gresiq-sdk = { path = "../smbcloud-cli/crates/smbcloud-gresiq-sdk" }
35
36
+candle-core = { path = "../candle/candle-core", features = ["metal", "accelerate"] }
37
+candle-nn = { path = "../candle/candle-nn" }
38
+tokenizers = { version = "0.20", default-features = false, features = ["onig"] }
39
+serde = { version = "1", features = ["derive"] }
40
+serde_json = "1"
41
+
42
# To publish to crates.io, switch back to version specs and remove path deps:
43
# smbcloud-auth-sdk = { version = "0.3" }
44
# smbcloud-model = { version = "0.3" }
src/app.rs
+220
@@ -41,6 +41,7 @@ pub enum Screen {
41
Models,
42
Downloads,
43
ModelDetail,
44
+ FineTune,
45
}
46
47
#[derive(Debug, Clone, PartialEq)]
@@ -55,6 +56,15 @@ pub enum Focus {
56
Password,
57
}
58
59
+#[derive(Debug, Clone, PartialEq)]
60
+pub enum FineTuneFocus {
61
+ ModelDir,
62
+ DataPath,
63
+ Rank,
64
+ Epochs,
65
+ Lr,
66
+}
67
+
68
#[derive(Debug, Clone, PartialEq)]
69
pub enum StatusTone {
70
Neutral,
@@ -123,6 +133,8 @@ pub enum AuthEvent {
133
DownloadsLoaded(Vec<crate::hf::MergedModel>),
134
#[allow(dead_code)] // reserved for future explicit error reporting
135
DownloadsLoadFailed(String),
136
+ // fine-tune
137
+ FineTuneProgress(crate::finetune::FineTuneProgress),
138
}
139
140
pub struct App {
@@ -160,6 +172,16 @@ pub struct App {
172
pub downloads_cursor: usize,
173
pub downloads_offset: usize,
174
pub downloads_loaded: bool,
175
+ // fine-tune
176
+ pub finetune_model_id: String,
177
+ pub finetune_model_dir: String,
178
+ pub finetune_data_path: String,
179
+ pub finetune_rank: String,
180
+ pub finetune_epochs: String,
181
+ pub finetune_lr: String,
182
+ pub finetune_focus: FineTuneFocus,
183
+ pub finetune_running: bool,
184
+ pub finetune_progress: Option<crate::finetune::FineTuneProgress>,
185
}
186
187
impl App {
@@ -191,6 +213,15 @@ impl App {
213
downloads_cursor: 0,
214
downloads_offset: 0,
215
downloads_loaded: false,
216
+ finetune_model_id: String::new(),
217
+ finetune_model_dir: String::new(),
218
+ finetune_data_path: "~/.onde/finetune/train.jsonl".to_string(),
219
+ finetune_rank: "8".to_string(),
220
+ finetune_epochs: "3".to_string(),
221
+ finetune_lr: "0.0001".to_string(),
222
+ finetune_focus: FineTuneFocus::ModelDir,
223
+ finetune_running: false,
224
+ finetune_progress: None,
225
}
226
}
227
@@ -351,6 +382,16 @@ impl App {
382
self.downloads_loaded = true;
383
self.status = Status::error(msg);
384
}
385
+ AuthEvent::FineTuneProgress(progress) => {
386
+ match &progress {
387
+ crate::finetune::FineTuneProgress::Done { .. }
388
+ | crate::finetune::FineTuneProgress::Failed(_) => {
389
+ self.finetune_running = false;
390
+ }
391
+ _ => {}
392
+ }
393
+ self.finetune_progress = Some(progress);
394
+ }
395
}
396
}
397
}
@@ -460,6 +501,7 @@ fn handle_key(
501
Screen::Models => handle_key_models(app, key, tx),
502
Screen::Downloads => handle_key_downloads(app, key),
503
Screen::ModelDetail => handle_key_model_detail(app, key),
504
+ Screen::FineTune => handle_key_finetune(app, key, tx),
505
}
506
}
507
@@ -753,6 +795,184 @@ fn handle_key_model_detail(app: &mut App, key: crossterm::event::KeyEvent) {
795
(Char('c'), KeyModifiers::CONTROL) => {
796
app.should_quit = true;
797
}
798
+ (Char('f'), KeyModifiers::NONE) => {
799
+ if let Some(model) = app.downloads.get(app.downloads_cursor) {
800
+ app.finetune_model_id = model.model_id.clone();
801
+ app.finetune_model_dir = resolve_hf_cache_path(&model.model_id);
802
+ app.finetune_focus = FineTuneFocus::DataPath;
803
+ app.finetune_running = false;
804
+ app.finetune_progress = None;
805
+ app.screen = Screen::FineTune;
806
+ app.status = Status::neutral("Configure fine-tuning.");
807
+ }
808
+ }
809
+ _ => {}
810
+ }
811
+}
812
+
813
+/// Resolve a HuggingFace model ID to its local cache snapshot directory.
814
+///
815
+/// Priority: App Group (onde-cli download cache) → `$HF_HOME` → `~/.cache/huggingface/hub`.
816
+/// Looks for `{hub}/models--{org}--{name}/snapshots/{hash}/` and returns the first match,
817
+/// or an empty string if not found.
818
+fn resolve_hf_cache_path(model_id: &str) -> String {
819
+ let dir_name = format!("models--{}", model_id.replace('/', "--"));
820
+
821
+ // App Group first (that's where onde-cli downloads), then standard HF cache
822
+ let candidates: Vec<std::path::PathBuf> = {
823
+ let mut c = Vec::new();
824
+ #[cfg(target_os = "macos")]
825
+ if let Some(home) = dirs::home_dir() {
826
+ c.push(
827
+ home.join("Library")
828
+ .join("Group Containers")
829
+ .join("group.com.ondeinference.apps")
830
+ .join("models")
831
+ .join("hub"),
832
+ );
833
+ }
834
+ // Check HF_HOME env
835
+ if let Ok(hf_home) = std::env::var("HF_HOME") {
836
+ c.push(std::path::PathBuf::from(hf_home).join("hub"));
837
+ }
838
+ // ~/.cache/huggingface/hub
839
+ if let Some(home) = dirs::home_dir() {
840
+ c.push(home.join(".cache").join("huggingface").join("hub"));
841
+ }
842
+ c
843
+ };
844
+
845
+ for hub in candidates {
846
+ let snapshots_dir = hub.join(&dir_name).join("snapshots");
847
+ if let Ok(entries) = std::fs::read_dir(&snapshots_dir) {
848
+ // Pick the first (usually only) snapshot hash directory
849
+ for entry in entries.flatten() {
850
+ let p = entry.path();
851
+ if p.is_dir() {
852
+ return p.to_string_lossy().to_string();
853
+ }
854
+ }
855
+ }
856
+ }
857
+
858
+ String::new()
859
+}
860
+
861
+fn handle_key_finetune(
862
+ app: &mut App,
863
+ key: crossterm::event::KeyEvent,
864
+ tx: mpsc::UnboundedSender<AuthEvent>,
865
+) {
866
+ use KeyCode::*;
867
+
868
+ if app.finetune_running {
869
+ if matches!(
870
+ (key.code, key.modifiers),
871
+ (Char('c'), KeyModifiers::CONTROL)
872
+ ) {
873
+ app.should_quit = true;
874
+ }
875
+ return;
876
+ }
877
+
878
+ match (key.code, key.modifiers) {
879
+ (Tab, _) => {
880
+ app.finetune_focus = match app.finetune_focus {
881
+ FineTuneFocus::ModelDir => FineTuneFocus::DataPath,
882
+ FineTuneFocus::DataPath => FineTuneFocus::Rank,
883
+ FineTuneFocus::Rank => FineTuneFocus::Epochs,
884
+ FineTuneFocus::Epochs => FineTuneFocus::Lr,
885
+ FineTuneFocus::Lr => FineTuneFocus::ModelDir,
886
+ };
887
+ }
888
+ (Backspace, _) => match app.finetune_focus {
889
+ FineTuneFocus::ModelDir => {
890
+ app.finetune_model_dir.pop();
891
+ }
892
+ FineTuneFocus::DataPath => {
893
+ app.finetune_data_path.pop();
894
+ }
895
+ FineTuneFocus::Rank => {
896
+ app.finetune_rank.pop();
897
+ }
898
+ FineTuneFocus::Epochs => {
899
+ app.finetune_epochs.pop();
900
+ }
901
+ FineTuneFocus::Lr => {
902
+ app.finetune_lr.pop();
903
+ }
904
+ },
905
+ (Enter, _) => {
906
+ let rank: usize = app.finetune_rank.parse().unwrap_or(8);
907
+ let epochs: usize = app.finetune_epochs.parse().unwrap_or(3);
908
+ let lr: f64 = app.finetune_lr.parse().unwrap_or(0.0001);
909
+
910
+ // Expand ~ in paths
911
+ let expand = |s: &str| -> String {
912
+ if let Some(rest) = s.strip_prefix("~/") {
913
+ if let Some(home) = dirs::home_dir() {
914
+ return home.join(rest).to_string_lossy().to_string();
915
+ }
916
+ }
917
+ s.to_string()
918
+ };
919
+
920
+ let model_dir = std::path::PathBuf::from(expand(&app.finetune_model_dir));
921
+ let data_path = std::path::PathBuf::from(expand(&app.finetune_data_path));
922
+ let output_dir = model_dir
923
+ .parent()
924
+ .unwrap_or(&model_dir)
925
+ .join("lora-adapter");
926
+
927
+ let config = crate::finetune::FineTuneConfig {
928
+ model_dir,
929
+ data_path,
930
+ output_dir,
931
+ lora_rank: rank,
932
+ lora_alpha: (rank as f32) * 2.0,
933
+ learning_rate: lr,
934
+ epochs,
935
+ max_seq_len: 512,
936
+ };
937
+
938
+ app.finetune_running = true;
939
+ app.finetune_progress = None;
940
+ app.status = Status::neutral("Fine-tuning started…");
941
+
942
+ let (ft_tx, mut ft_rx) = mpsc::unbounded_channel();
943
+ crate::finetune::start_finetune(config, ft_tx);
944
+
945
+ let auth_tx = tx.clone();
946
+ tokio::spawn(async move {
947
+ while let Some(progress) = ft_rx.recv().await {
948
+ let _ = auth_tx.send(AuthEvent::FineTuneProgress(progress));
949
+ }
950
+ });
951
+ }
952
+ (Esc, _) => {
953
+ app.screen = Screen::ModelDetail;
954
+ app.status = Status::neutral("Back to model.");
955
+ }
956
+ (Char('c'), KeyModifiers::CONTROL) => {
957
+ app.should_quit = true;
958
+ }
959
+ (Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => match app.finetune_focus {
960
+ FineTuneFocus::ModelDir => {
961
+ app.finetune_model_dir.push(c);
962
+ }
963
+ FineTuneFocus::DataPath => {
964
+ app.finetune_data_path.push(c);
965
+ }
966
+ FineTuneFocus::Rank => {
967
+ app.finetune_rank.push(c);
968
+ }
969
+ FineTuneFocus::Epochs => {
970
+ app.finetune_epochs.push(c);
971
+ }
972
+ FineTuneFocus::Lr => {
973
+ app.finetune_lr.push(c);
974
+ }
975
+ },
976
_ => {}
977
}
978
}
src/finetune.rs
new
+822
@@ -0,0 +1,822 @@
1
+//! LoRA fine-tuning for Qwen-family models (Qwen2, Qwen2.5, Qwen3).
2
+//!
3
+//! Loads a safetensors base model from a HuggingFace cache directory, injects
4
+//! trainable LoRA adapters into the Q and V projection layers of every
5
+//! transformer block, trains for a configurable number of epochs on a JSONL
6
+//! dataset, and saves the resulting adapter weights to a safetensors file.
7
+
8
+use std::collections::{HashMap, HashSet};
9
+use std::io::{BufRead, BufReader};
10
+use std::path::{Path, PathBuf};
11
+
12
+use anyhow::{Context, Result};
13
+use candle_core::{D, DType, Device, Tensor, Var};
14
+use candle_nn::{AdamW, Optimizer, ParamsAdamW, VarBuilder};
15
+use tokenizers::Tokenizer;
16
+
17
+// ---------------------------------------------------------------------------
18
+// Public types
19
+// ---------------------------------------------------------------------------
20
+
21
+/// Configuration for a LoRA fine-tuning run.
22
+pub struct FineTuneConfig {
23
+ /// Directory that contains `config.json`, `tokenizer.json`, and the
24
+ /// safetensors weight files (single-file or sharded).
25
+ pub model_dir: PathBuf,
26
+ /// JSONL file where every line is `{"text": "…"}`.
27
+ pub data_path: PathBuf,
28
+ /// Directory where `lora_adapter.safetensors` will be written.
29
+ pub output_dir: PathBuf,
30
+ /// LoRA rank (default 8).
31
+ pub lora_rank: usize,
32
+ /// LoRA alpha, used to compute the scaling factor `alpha / rank` (default 16.0).
33
+ pub lora_alpha: f32,
34
+ /// Learning rate for AdamW (default 1e-4).
35
+ pub learning_rate: f64,
36
+ /// Number of full passes over the dataset (default 3).
37
+ pub epochs: usize,
38
+ /// Maximum number of tokens per training example (default 512).
39
+ pub max_seq_len: usize,
40
+}
41
+
42
+/// Progress events emitted by the background fine-tuning thread.
43
+pub enum FineTuneProgress {
44
+ /// Checking that the model directory and data file exist.
45
+ Validating,
46
+ /// Loading and memory-mapping the model weights.
47
+ LoadingModel,
48
+ /// Tokenizing the dataset.
49
+ Tokenizing { done: usize, total: usize },
50
+ /// One optimizer step has completed.
51
+ Training {
52
+ epoch: usize,
53
+ total_epochs: usize,
54
+ step: usize,
55
+ total_steps: usize,
56
+ loss: f32,
57
+ },
58
+ /// Writing the adapter file to disk.
59
+ Saving,
60
+ /// Training finished successfully.
61
+ Done { adapter_path: PathBuf },
62
+ /// An unrecoverable error occurred.
63
+ Failed(String),
64
+}
65
+
66
+// ---------------------------------------------------------------------------
67
+// Public API
68
+// ---------------------------------------------------------------------------
69
+
70
+/// Spawn a background OS thread that runs the full fine-tuning pipeline and
71
+/// streams [`FineTuneProgress`] events over `tx`.
72
+///
73
+/// The thread is intentionally a blocking `std::thread` (not a tokio task) so
74
+/// that heavy tensor operations do not starve the async runtime.
75
+pub fn start_finetune(
76
+ config: FineTuneConfig,
77
+ tx: tokio::sync::mpsc::UnboundedSender<FineTuneProgress>,
78
+) {
79
+ std::thread::spawn(move || {
80
+ if let Err(e) = run_finetune(&config, &tx) {
81
+ let _ = tx.send(FineTuneProgress::Failed(format!("{e:#}")));
82
+ }
83
+ });
84
+}
85
+
86
+// ---------------------------------------------------------------------------
87
+// Private: top-level orchestration
88
+// ---------------------------------------------------------------------------
89
+
90
+fn run_finetune(
91
+ config: &FineTuneConfig,
92
+ tx: &tokio::sync::mpsc::UnboundedSender<FineTuneProgress>,
93
+) -> Result<()> {
94
+ // ------------------------------------------------------------------
95
+ // Step A — Validate inputs
96
+ // ------------------------------------------------------------------
97
+ let _ = tx.send(FineTuneProgress::Validating);
98
+
99
+ let model_dir = &config.model_dir;
100
+ let config_json_path = model_dir.join("config.json");
101
+
102
+ if !config_json_path.exists() {
103
+ anyhow::bail!(
104
+ "model config not found at {:?}; is this a valid HuggingFace model directory?",
105
+ config_json_path
106
+ );
107
+ }
108
+ if !config.data_path.exists() {
109
+ anyhow::bail!("training data file not found at {:?}", config.data_path);
110
+ }
111
+
112
+ // ------------------------------------------------------------------
113
+ // Step B — Load model config
114
+ // ------------------------------------------------------------------
115
+ let config_text = std::fs::read_to_string(&config_json_path)
116
+ .with_context(|| format!("reading {:?}", config_json_path))?;
117
+ let model_cfg: ModelConfig = serde_json::from_str(&config_text)
118
+ .with_context(|| format!("parsing {:?}", config_json_path))?;
119
+
120
+ // ------------------------------------------------------------------
121
+ // Step C — Choose compute device
122
+ // ------------------------------------------------------------------
123
+ #[cfg(target_os = "macos")]
124
+ let device = Device::new_metal(0).unwrap_or(Device::Cpu);
125
+ #[cfg(not(target_os = "macos"))]
126
+ let device = Device::Cpu;
127
+
128
+ // ------------------------------------------------------------------
129
+ // Step D — Load frozen base weights
130
+ // ------------------------------------------------------------------
131
+ let _ = tx.send(FineTuneProgress::LoadingModel);
132
+
133
+ let index_path = model_dir.join("model.safetensors.index.json");
134
+ let vb = if index_path.exists() {
135
+ // Sharded model: read the weight-map JSON and collect unique shard paths.
136
+ let index_text = std::fs::read_to_string(&index_path)
137
+ .with_context(|| format!("reading {:?}", index_path))?;
138
+ let index: IndexJson = serde_json::from_str(&index_text)
139
+ .with_context(|| format!("parsing {:?}", index_path))?;
140
+
141
+ let mut seen: HashSet<String> = HashSet::new();
142
+ let mut shard_paths: Vec<PathBuf> = Vec::new();
143
+ for filename in index.weight_map.values() {
144
+ if seen.insert(filename.clone()) {
145
+ shard_paths.push(model_dir.join(filename));
146
+ }
147
+ }
148
+ shard_paths.sort();
149
+
150
+ // SAFETY: the files are memory-mapped read-only.
151
+ unsafe {
152
+ VarBuilder::from_mmaped_safetensors(&shard_paths, DType::F32, &device)
153
+ .context("loading sharded safetensors")?
154
+ }
155
+ } else {
156
+ let single = model_dir.join("model.safetensors");
157
+ // SAFETY: the file is memory-mapped read-only.
158
+ unsafe {
159
+ VarBuilder::from_mmaped_safetensors(&[single], DType::F32, &device)
160
+ .context("loading model.safetensors")?
161
+ }
162
+ };
163
+
164
+ // ------------------------------------------------------------------
165
+ // Step E — Build LoRA model
166
+ // ------------------------------------------------------------------
167
+ let model = LoraQwenModel::load(vb, &model_cfg, config.lora_rank, config.lora_alpha, &device)
168
+ .context("building LoRA model")?;
169
+
170
+ // ------------------------------------------------------------------
171
+ // Step F — Load and tokenize dataset
172
+ // ------------------------------------------------------------------
173
+ let tokenizer_path = model_dir.join("tokenizer.json");
174
+ let tokenizer = Tokenizer::from_file(&tokenizer_path)
175
+ .map_err(|e| anyhow::anyhow!("loading tokenizer from {:?}: {e}", tokenizer_path))?;
176
+
177
+ let file = std::fs::File::open(&config.data_path)
178
+ .with_context(|| format!("opening {:?}", config.data_path))?;
179
+ let reader = BufReader::new(file);
180
+ let raw_lines: Vec<String> = reader
181
+ .lines()
182
+ .collect::<std::io::Result<Vec<_>>>()
183
+ .context("reading training data")?;
184
+
185
+ let total_lines = raw_lines.len();
186
+ let mut token_batches: Vec<Vec<u32>> = Vec::new();
187
+
188
+ for (i, line) in raw_lines.iter().enumerate() {
189
+ let line = line.trim();
190
+ if line.is_empty() {
191
+ let _ = tx.send(FineTuneProgress::Tokenizing {
192
+ done: i + 1,
193
+ total: total_lines,
194
+ });
195
+ continue;
196
+ }
197
+
198
+ let entry: DataEntry =
199
+ serde_json::from_str(line).with_context(|| format!("parsing JSONL line {}", i + 1))?;
200
+
201
+ let encoding = tokenizer
202
+ .encode(entry.text.as_str(), false)
203
+ .map_err(|e| anyhow::anyhow!("tokenizing line {}: {e}", i + 1))?;
204
+
205
+ let ids = encoding.get_ids();
206
+ let ids: Vec<u32> = if ids.len() > config.max_seq_len {
207
+ ids[..config.max_seq_len].to_vec()
208
+ } else {
209
+ ids.to_vec()
210
+ };
211
+
212
+ // Need at least 2 tokens for next-token prediction.
213
+ if ids.len() >= 2 {
214
+ token_batches.push(ids);
215
+ }
216
+
217
+ let _ = tx.send(FineTuneProgress::Tokenizing {
218
+ done: i + 1,
219
+ total: total_lines,
220
+ });
221
+ }
222
+
223
+ if token_batches.is_empty() {
224
+ anyhow::bail!("no valid training examples found in {:?}", config.data_path);
225
+ }
226
+
227
+ // ------------------------------------------------------------------
228
+ // Step G — Training loop
229
+ // ------------------------------------------------------------------
230
+ let lora_vars = model.lora_vars();
231
+ let adamw_params = ParamsAdamW {
232
+ lr: config.learning_rate,
233
+ ..ParamsAdamW::default()
234
+ };
235
+ let mut optimizer = AdamW::new(lora_vars, adamw_params).context("creating AdamW optimizer")?;
236
+
237
+ let total_steps = token_batches.len();
238
+ let vocab_size = model_cfg.vocab_size;
239
+
240
+ for epoch in 0..config.epochs {
241
+ for (step, batch_tokens) in token_batches.iter().enumerate() {
242
+ let seq_len = batch_tokens.len();
243
+
244
+ let input_ids = Tensor::from_slice(batch_tokens.as_slice(), (1, seq_len), &device)
245
+ .context("building input_ids tensor")?;
246
+
247
+ let logits = model.forward(&input_ids).context("model forward pass")?;
248
+
249
+ // Next-token prediction: shift logits and targets by 1.
250
+ let logits_shifted = logits
251
+ .narrow(1, 0, seq_len - 1)
252
+ .and_then(|t| t.reshape((seq_len - 1, vocab_size)))
253
+ .context("preparing shifted logits")?;
254
+
255
+ let targets = Tensor::from_slice(&batch_tokens[1..], (seq_len - 1,), &device)
256
+ .context("building target tensor")?;
257
+
258
+ let loss = candle_nn::loss::cross_entropy(&logits_shifted, &targets)
259
+ .context("computing cross-entropy loss")?;
260
+
261
+ optimizer.backward_step(&loss).context("optimizer step")?;
262
+
263
+ let loss_val = loss.to_scalar::<f32>().context("reading loss scalar")?;
264
+
265
+ let _ = tx.send(FineTuneProgress::Training {
266
+ epoch: epoch + 1,
267
+ total_epochs: config.epochs,
268
+ step: step + 1,
269
+ total_steps,
270
+ loss: loss_val,
271
+ });
272
+ }
273
+ }
274
+
275
+ // ------------------------------------------------------------------
276
+ // Step H — Save LoRA adapter
277
+ // ------------------------------------------------------------------
278
+ let _ = tx.send(FineTuneProgress::Saving);
279
+
280
+ let adapter_path = model
281
+ .save_lora(&config.output_dir)
282
+ .context("saving LoRA adapter")?;
283
+
284
+ let _ = tx.send(FineTuneProgress::Done { adapter_path });
285
+
286
+ Ok(())
287
+}
288
+
289
+// ---------------------------------------------------------------------------
290
+// Private: serde types for JSON deserialization
291
+// ---------------------------------------------------------------------------
292
+
293
+#[derive(serde::Deserialize)]
294
+struct ModelConfig {
295
+ hidden_size: usize,
296
+ num_hidden_layers: usize,
297
+ num_attention_heads: usize,
298
+ num_key_value_heads: Option<usize>,
299
+ intermediate_size: usize,
300
+ vocab_size: usize,
301
+ rms_norm_eps: Option<f64>,
302
+ rope_theta: Option<f64>,
303
+}
304
+
305
+#[derive(serde::Deserialize)]
306
+struct DataEntry {
307
+ text: String,
308
+}
309
+
310
+/// Top-level structure of `model.safetensors.index.json`.
311
+#[derive(serde::Deserialize)]
312
+struct IndexJson {
313
+ weight_map: HashMap<String, String>,
314
+}
315
+
316
+// ---------------------------------------------------------------------------
317
+// Private: tensor helpers
318
+// ---------------------------------------------------------------------------
319
+
320
+/// RMS layer normalisation: `x / rms(x) * weight`.
321
+///
322
+/// `weight` has shape `[hidden_size]`; `x` has shape `[batch, seq, hidden_size]`.
323
+fn rms_norm(x: &Tensor, weight: &Tensor, eps: f64) -> candle_core::Result<Tensor> {
324
+ // mean of squares across the last (hidden) dimension → [batch, seq, 1]
325
+ let mean_sq = x.sqr()?.mean_keepdim(D::Minus1)?;
326
+ // x / sqrt(mean_sq + eps)
327
+ let normed = x.broadcast_div(&mean_sq.affine(1.0, eps)?.sqrt()?)?;
328
+ // scale by the learned weight
329
+ normed.broadcast_mul(weight)
330
+}
331
+
332
+/// Apply rotary position embeddings to `x`.
333
+///
334
+/// `x` has shape `[batch, heads, seq, head_dim]`.
335
+/// `cos` has shape `[seq, head_dim]` (already narrowed to the current sequence length).
336
+/// `sin` has shape `[seq, head_dim]`.
337
+fn apply_rope(x: &Tensor, cos: &Tensor, sin: &Tensor) -> candle_core::Result<Tensor> {
338
+ let head_dim = x.dim(D::Minus1)?;
339
+ let half = head_dim / 2;
340
+
341
+ let x1 = x.narrow(D::Minus1, 0, half)?; // first half
342
+ let x2 = x.narrow(D::Minus1, half, half)?; // second half
343
+
344
+ // rotate_half: [-x2, x1]
345
+ let neg_x2 = x2.neg()?;
346
+ let rotated = Tensor::cat(&[&neg_x2, &x1], D::Minus1)?;
347
+
348
+ // Unsqueeze cos/sin to [1, 1, seq, head_dim] for broadcasting.
349
+ let cos = cos.unsqueeze(0)?.unsqueeze(0)?;
350
+ let sin = sin.unsqueeze(0)?.unsqueeze(0)?;
351
+
352
+ x.broadcast_mul(&cos)? + rotated.broadcast_mul(&sin)?
353
+}
354
+
355
+/// Precompute cosine and sine tables for RoPE.
356
+///
357
+/// Returns `(cos, sin)` each with shape `[max_seq_len, head_dim]`.
358
+fn precompute_rope(
359
+ head_dim: usize,
360
+ max_seq_len: usize,
361
+ theta: f64,
362
+ device: &Device,
363
+) -> candle_core::Result<(Tensor, Tensor)> {
364
+ let half = head_dim / 2;
365
+
366
+ // Inverse frequencies: 1 / theta^(2i / head_dim) for i in 0..half
367
+ let freqs: Vec<f32> = (0..half)
368
+ .map(|i| 1.0f32 / (theta as f32).powf(2.0 * i as f32 / head_dim as f32))
369
+ .collect();
370
+
371
+ // freqs: [1, half]
372
+ let freqs = Tensor::from_slice(freqs.as_slice(), (1, half), device)?;
373
+
374
+ // position indices: [max_seq_len, 1]
375
+ let pos: Vec<f32> = (0..max_seq_len).map(|i| i as f32).collect();
376
+ let pos = Tensor::from_slice(pos.as_slice(), (max_seq_len, 1), device)?;
377
+
378
+ // outer product → [max_seq_len, half]
379
+ let freqs = pos.broadcast_mul(&freqs)?;
380
+
381
+ // Duplicate halves so both halves receive the same rotation angle.
382
+ // Result shape: [max_seq_len, head_dim]
383
+ let cos = Tensor::cat(&[&freqs.cos()?, &freqs.cos()?], 1)?;
384
+ let sin = Tensor::cat(&[&freqs.sin()?, &freqs.sin()?], 1)?;
385
+
386
+ Ok((cos, sin))
387
+}
388
+
389
+// ---------------------------------------------------------------------------
390
+// Private: LoRA-augmented linear layer
391
+// ---------------------------------------------------------------------------
392
+
393
+struct LoraLinear {
394
+ /// Frozen base weight `[out_features, in_features]`.
395
+ weight: Tensor,
396
+ /// Optional frozen base bias `[out_features]`.
397
+ bias: Option<Tensor>,
398
+ /// Trainable LoRA A matrix `[rank, in_features]`, initialised N(0, 1/√rank).
399
+ lora_a: Var,
400
+ /// Trainable LoRA B matrix `[out_features, rank]`, initialised to zeros.
401
+ lora_b: Var,
402
+ /// Scaling factor `alpha / rank`.
403
+ scale: f64,
404
+}
405
+
406
+impl LoraLinear {
407
+ fn new(
408
+ weight: Tensor,
409
+ bias: Option<Tensor>,
410
+ rank: usize,
411
+ alpha: f32,
412
+ device: &Device,
413
+ ) -> candle_core::Result<Self> {
414
+ let shape = weight.shape().dims();
415
+ // weight is [out_features, in_features]
416
+ let out_features = shape[0];
417
+ let in_features = shape[1];
418
+
419
+ let std_val = (1.0 / (rank as f64).sqrt()) as f32;
420
+ let lora_a = Var::randn(0.0f32, std_val, (rank, in_features), device)?;
421
+ let lora_b = Var::zeros((out_features, rank), DType::F32, device)?;
422
+ let scale = alpha as f64 / rank as f64;
423
+
424
+ Ok(Self {
425
+ weight,
426
+ bias,
427
+ lora_a,
428
+ lora_b,
429
+ scale,
430
+ })
431
+ }
432
+
433
+ /// `y = x W^T + b + (x A^T) B^T * scale`
434
+ fn forward(&self, x: &Tensor) -> candle_core::Result<Tensor> {
435
+ // Base projection
436
+ let base = x.matmul(&self.weight.t()?)?;
437
+ let base = match &self.bias {
438
+ Some(b) => base.broadcast_add(b)?,
439
+ None => base,
440
+ };
441
+
442
+ // LoRA path: x A^T → x B^T scaled
443
+ let lora = x
444
+ .matmul(&self.lora_a.as_tensor().t()?)?
445
+ .matmul(&self.lora_b.as_tensor().t()?)?
446
+ .affine(self.scale, 0.0)?;
447
+
448
+ base + lora
449
+ }
450
+
451
+ /// Return the two trainable LoRA variables.
452
+ fn vars(&self) -> Vec<Var> {
453
+ vec![self.lora_a.clone(), self.lora_b.clone()]
454
+ }
455
+}
456
+
457
+// ---------------------------------------------------------------------------
458
+// Private: frozen linear layer (no LoRA)
459
+// ---------------------------------------------------------------------------
460
+
461
+struct FrozenLinear {
462
+ weight: Tensor,
463
+ bias: Option<Tensor>,
464
+}
465
+
466
+impl FrozenLinear {
467
+ fn forward(&self, x: &Tensor) -> candle_core::Result<Tensor> {
468
+ let out = x.matmul(&self.weight.t()?)?;
469
+ match &self.bias {
470
+ Some(b) => out.broadcast_add(b),
471
+ None => Ok(out),
472
+ }
473
+ }
474
+}
475
+
476
+// ---------------------------------------------------------------------------
477
+// Private: single transformer block
478
+// ---------------------------------------------------------------------------
479
+
480
+struct TransformerLayer {
481
+ // Attention projections
482
+ q_proj: LoraLinear, // LoRA applied
483
+ k_proj: FrozenLinear, // frozen
484
+ v_proj: LoraLinear, // LoRA applied
485
+ o_proj: FrozenLinear, // frozen
486
+ // MLP projections (SwiGLU)
487
+ gate_proj: FrozenLinear,
488
+ up_proj: FrozenLinear,
489
+ down_proj: FrozenLinear,
490
+ // LayerNorm weights (RMSNorm, no bias)
491
+ input_layernorm: Tensor, // [hidden_size]
492
+ post_attention_layernorm: Tensor, // [hidden_size]
493
+ // Hyperparameters
494
+ num_heads: usize,
495
+ num_kv_heads: usize,
496
+ head_dim: usize,
497
+ rms_norm_eps: f64,
498
+}
499
+
500
+impl TransformerLayer {
501
+ /// Full pre-norm transformer block forward pass.
502
+ ///
503
+ /// `x` has shape `[batch, seq, hidden_size]`.
504
+ /// `cos` / `sin` have shape `[seq, head_dim]` (sliced from the precomputed tables).
505
+ fn forward(&self, x: &Tensor, cos: &Tensor, sin: &Tensor) -> candle_core::Result<Tensor> {
506
+ let (b, seq, _hidden) = x.dims3()?;
507
+
508
+ // ----- Attention sub-layer -----
509
+
510
+ // Pre-attention RMSNorm
511
+ let normed = rms_norm(x, &self.input_layernorm, self.rms_norm_eps)?;
512
+
513
+ // QKV projections
514
+ let q = self.q_proj.forward(&normed)?; // [b, seq, num_heads * head_dim]
515
+ let k = self.k_proj.forward(&normed)?; // [b, seq, num_kv_heads * head_dim]
516
+ let v = self.v_proj.forward(&normed)?; // [b, seq, num_kv_heads * head_dim]
517
+
518
+ // Reshape to [b, heads, seq, head_dim]
519
+ let q = q
520
+ .reshape((b, seq, self.num_heads, self.head_dim))?
521
+ .transpose(1, 2)?;
522
+ let k = k
523
+ .reshape((b, seq, self.num_kv_heads, self.head_dim))?
524
+ .transpose(1, 2)?;
525
+ let v = v
526
+ .reshape((b, seq, self.num_kv_heads, self.head_dim))?
527
+ .transpose(1, 2)?;
528
+
529
+ // Apply RoPE (requires contiguous storage for the custom kernel)
530
+ let q = apply_rope(&q.contiguous()?, cos, sin)?;
531
+ let k = apply_rope(&k.contiguous()?, cos, sin)?;
532
+
533
+ // Grouped Query Attention: expand K/V heads if num_heads > num_kv_heads
534
+ let groups = self.num_heads / self.num_kv_heads;
535
+ let (k, v) = if groups == 1 {
536
+ (k.contiguous()?, v.contiguous()?)
537
+ } else {
538
+ let k_exp = k
539
+ .unsqueeze(2)?
540
+ .expand((b, self.num_kv_heads, groups, seq, self.head_dim))?
541
+ .reshape((b, self.num_heads, seq, self.head_dim))?
542
+ .contiguous()?;
543
+ let v_exp = v
544
+ .unsqueeze(2)?
545
+ .expand((b, self.num_kv_heads, groups, seq, self.head_dim))?
546
+ .reshape((b, self.num_heads, seq, self.head_dim))?
547
+ .contiguous()?;
548
+ (k_exp, v_exp)
549
+ };
550
+
551
+ // Scaled dot-product attention scores
552
+ let scale = 1.0 / (self.head_dim as f64).sqrt();
553
+ let attn_weights = (q.matmul(&k.transpose(2, 3)?)? * scale)?;
554
+
555
+ // Causal mask: add -1e9 to positions the query must not attend to.
556
+ let device = x.device();
557
+ let tril =
558
+ Tensor::tril2(seq, DType::F32, device)?.broadcast_as((b, self.num_heads, seq, seq))?;
559
+ // (1 - tril) is 1 on upper triangle (blocked), 0 on lower triangle (allowed).
560
+ let attn_bias = (1.0f64 - &tril)?.affine(-1e9, 0.0)?;
561
+ let attn_weights = (attn_weights + attn_bias)?;
562
+ let attn_weights = candle_nn::ops::softmax_last_dim(&attn_weights)?;
563
+
564
+ // Weighted sum of values
565
+ let attn_out = attn_weights.matmul(&v)?;
566
+
567
+ // Merge heads: [b, heads, seq, head_dim] → [b, seq, hidden_size]
568
+ let attn_out = attn_out.transpose(1, 2)?.contiguous()?.reshape((
569
+ b,
570
+ seq,
571
+ self.num_heads * self.head_dim,
572
+ ))?;
573
+
574
+ // Output projection
575
+ let attn_out = self.o_proj.forward(&attn_out)?;
576
+
577
+ // First residual connection
578
+ let h = (x + &attn_out)?;
579
+
580
+ // ----- MLP sub-layer (SwiGLU) -----
581
+
582
+ // Pre-MLP RMSNorm
583
+ let normed2 = rms_norm(&h, &self.post_attention_layernorm, self.rms_norm_eps)?;
584
+
585
+ let gate = candle_nn::ops::silu(&self.gate_proj.forward(&normed2)?)?;
586
+ let up = self.up_proj.forward(&normed2)?;
587
+ let mlp_out = self.down_proj.forward(&(gate * up)?)?;
588
+
589
+ // Second residual connection
590
+ h + mlp_out
591
+ }
592
+
593
+ /// All trainable LoRA variables in this layer.
594
+ fn lora_vars(&self) -> Vec<Var> {
595
+ let mut v = self.q_proj.vars();
596
+ v.extend(self.v_proj.vars());
597
+ v
598
+ }
599
+}
600
+
601
+// ---------------------------------------------------------------------------
602
+// Private: full model
603
+// ---------------------------------------------------------------------------
604
+
605
+struct LoraQwenModel {
606
+ embed_tokens: Tensor, // [vocab_size, hidden_size]
607
+ layers: Vec<TransformerLayer>,
608
+ norm: Tensor, // [hidden_size] — final RMSNorm weight
609
+ lm_head: Tensor, // [vocab_size, hidden_size]
610
+ rope_cos: Tensor, // [max_seq_len, head_dim]
611
+ rope_sin: Tensor, // [max_seq_len, head_dim]
612
+ hidden_size: usize,
613
+ vocab_size: usize,
614
+ rms_norm_eps: f64,
615
+}
616
+
617
+impl LoraQwenModel {
618
+ /// Load base weights from a `VarBuilder` and inject LoRA adapters.
619
+ fn load(
620
+ vb: VarBuilder,
621
+ cfg: &ModelConfig,
622
+ rank: usize,
623
+ alpha: f32,
624
+ device: &Device,
625
+ ) -> candle_core::Result<Self> {
626
+ let hidden_size = cfg.hidden_size;
627
+ let vocab_size = cfg.vocab_size;
628
+ let num_heads = cfg.num_attention_heads;
629
+ let num_kv_heads = cfg.num_key_value_heads.unwrap_or(num_heads);
630
+ let head_dim = hidden_size / num_heads;
631
+ let intermediate_size = cfg.intermediate_size;
632
+ let rms_norm_eps = cfg.rms_norm_eps.unwrap_or(1e-6);
633
+ let rope_theta = cfg.rope_theta.unwrap_or(10_000.0);
634
+
635
+ // Maximum sequence length for the RoPE tables.
636
+ // 2048 covers all models targeted by this module; increase if needed.
637
+ let max_seq_len = 2048usize;
638
+
639
+ // Embedding table
640
+ let embed_tokens = vb
641
+ .pp("model")
642
+ .pp("embed_tokens")
643
+ .get((vocab_size, hidden_size), "weight")?;
644
+
645
+ // Transformer layers
646
+ let mut layers: Vec<TransformerLayer> = Vec::with_capacity(cfg.num_hidden_layers);
647
+
648
+ for i in 0..cfg.num_hidden_layers {
649
+ let vb_layer = vb.pp("model").pp("layers").pp(i.to_string());
650
+
651
+ let vb_attn = vb_layer.pp("self_attn");
652
+
653
+ // --- Q projection (LoRA) ---
654
+ let q_out = num_heads * head_dim;
655
+ let q_weight = vb_attn.pp("q_proj").get((q_out, hidden_size), "weight")?;
656
+ // Qwen2 has bias on q/k/v; Qwen3 does not — use .ok() to handle both.
657
+ let q_bias = vb_attn.pp("q_proj").get((q_out,), "bias").ok();
658
+ let q_proj = LoraLinear::new(q_weight, q_bias, rank, alpha, device)?;
659
+
660
+ // --- K projection (frozen) ---
661
+ let kv_out = num_kv_heads * head_dim;
662
+ let k_weight = vb_attn.pp("k_proj").get((kv_out, hidden_size), "weight")?;
663
+ let k_bias = vb_attn.pp("k_proj").get((kv_out,), "bias").ok();
664
+ let k_proj = FrozenLinear {
665
+ weight: k_weight,
666
+ bias: k_bias,
667
+ };
668
+
669
+ // --- V projection (LoRA) ---
670
+ let v_weight = vb_attn.pp("v_proj").get((kv_out, hidden_size), "weight")?;
671
+ let v_bias = vb_attn.pp("v_proj").get((kv_out,), "bias").ok();
672
+ let v_proj = LoraLinear::new(v_weight, v_bias, rank, alpha, device)?;
673
+
674
+ // --- O projection (frozen) ---
675
+ let o_weight = vb_attn
676
+ .pp("o_proj")
677
+ .get((hidden_size, num_heads * head_dim), "weight")?;
678
+ let o_proj = FrozenLinear {
679
+ weight: o_weight,
680
+ bias: None,
681
+ };
682
+
683
+ // --- MLP projections (all frozen) ---
684
+ let vb_mlp = vb_layer.pp("mlp");
685
+
686
+ let gate_weight = vb_mlp
687
+ .pp("gate_proj")
688
+ .get((intermediate_size, hidden_size), "weight")?;
689
+ let up_weight = vb_mlp
690
+ .pp("up_proj")
691
+ .get((intermediate_size, hidden_size), "weight")?;
692
+ let down_weight = vb_mlp
693
+ .pp("down_proj")
694
+ .get((hidden_size, intermediate_size), "weight")?;
695
+
696
+ // --- LayerNorm weights ---
697
+ let input_layernorm = vb_layer
698
+ .pp("input_layernorm")
699
+ .get((hidden_size,), "weight")?;
700
+ let post_attention_layernorm = vb_layer
701
+ .pp("post_attention_layernorm")
702
+ .get((hidden_size,), "weight")?;
703
+
704
+ layers.push(TransformerLayer {
705
+ q_proj,
706
+ k_proj,
707
+ v_proj,
708
+ o_proj,
709
+ gate_proj: FrozenLinear {
710
+ weight: gate_weight,
711
+ bias: None,
712
+ },
713
+ up_proj: FrozenLinear {
714
+ weight: up_weight,
715
+ bias: None,
716
+ },
717
+ down_proj: FrozenLinear {
718
+ weight: down_weight,
719
+ bias: None,
720
+ },
721
+ input_layernorm,
722
+ post_attention_layernorm,
723
+ num_heads,
724
+ num_kv_heads,
725
+ head_dim,
726
+ rms_norm_eps,
727
+ });
728
+ }
729
+
730
+ // Final RMSNorm and language model head
731
+ let norm = vb.pp("model").pp("norm").get((hidden_size,), "weight")?;
732
+ let lm_head = vb.pp("lm_head").get((vocab_size, hidden_size), "weight")?;
733
+
734
+ // Precompute RoPE tables
735
+ let (rope_cos, rope_sin) = precompute_rope(head_dim, max_seq_len, rope_theta, device)?;
736
+
737
+ Ok(Self {
738
+ embed_tokens,
739
+ layers,
740
+ norm,
741
+ lm_head,
742
+ rope_cos,
743
+ rope_sin,
744
+ hidden_size,
745
+ vocab_size,
746
+ rms_norm_eps,
747
+ })
748
+ }
749
+
750
+ /// Full forward pass.
751
+ ///
752
+ /// `input_ids` has shape `[batch, seq]`.
753
+ /// Returns logits with shape `[batch, seq, vocab_size]`.
754
+ fn forward(&self, input_ids: &Tensor) -> candle_core::Result<Tensor> {
755
+ let (b, seq) = input_ids.dims2()?;
756
+
757
+ // Token embedding lookup: [batch, seq, hidden_size]
758
+ let ids_flat = input_ids.flatten_all()?; // [b * seq]
759
+ let mut hidden = self
760
+ .embed_tokens
761
+ .embedding(&ids_flat)? // [b * seq, hidden_size]
762
+ .reshape((b, seq, self.hidden_size))?; // [b, seq, hidden_size]
763
+
764
+ // Slice RoPE tables to the current sequence length.
765
+ let cos = self.rope_cos.narrow(0, 0, seq)?; // [seq, head_dim]
766
+ let sin = self.rope_sin.narrow(0, 0, seq)?; // [seq, head_dim]
767
+
768
+ // Transformer layers
769
+ for layer in &self.layers {
770
+ hidden = layer.forward(&hidden, &cos, &sin)?;
771
+ }
772
+
773
+ // Final RMSNorm
774
+ let hidden = rms_norm(&hidden, &self.norm, self.rms_norm_eps)?;
775
+
776
+ // Language model head: [b, seq, vocab_size]
777
+ hidden.matmul(&self.lm_head.t()?)
778
+ }
779
+
780
+ /// Collect all trainable LoRA variables across every layer.
781
+ fn lora_vars(&self) -> Vec<Var> {
782
+ self.layers
783
+ .iter()
784
+ .flat_map(|layer| layer.lora_vars())
785
+ .collect()
786
+ }
787
+
788
+ /// Serialise the LoRA adapter weights to `output_dir/lora_adapter.safetensors`.
789
+ ///
790
+ /// Saves one `lora_a` and one `lora_b` tensor per projection (Q and V) per layer.
791
+ fn save_lora(&self, output_dir: &Path) -> Result<PathBuf> {
792
+ std::fs::create_dir_all(output_dir)
793
+ .with_context(|| format!("creating output directory {:?}", output_dir))?;
794
+
795
+ let mut tensors: HashMap<String, Tensor> = HashMap::new();
796
+
797
+ for (i, layer) in self.layers.iter().enumerate() {
798
+ tensors.insert(
799
+ format!("model.layers.{i}.self_attn.q_proj.lora_a"),
800
+ layer.q_proj.lora_a.as_tensor().clone(),
801
+ );
802
+ tensors.insert(
803
+ format!("model.layers.{i}.self_attn.q_proj.lora_b"),
804
+ layer.q_proj.lora_b.as_tensor().clone(),
805
+ );
806
+ tensors.insert(
807
+ format!("model.layers.{i}.self_attn.v_proj.lora_a"),
808
+ layer.v_proj.lora_a.as_tensor().clone(),
809
+ );
810
+ tensors.insert(
811
+ format!("model.layers.{i}.self_attn.v_proj.lora_b"),
812
+ layer.v_proj.lora_b.as_tensor().clone(),
813
+ );
814
+ }
815
+
816
+ let path = output_dir.join("lora_adapter.safetensors");
817
+ candle_core::safetensors::save(&tensors, &path)
818
+ .with_context(|| format!("writing {:?}", path))?;
819
+
820
+ Ok(path)
821
+ }
822
+}
src/main.rs
+1
@@ -1,4 +1,5 @@
1
mod app;
2
+mod finetune;
3
mod gresiq;
4
mod hf;
5
mod token;
src/ui.rs
+289
-1
@@ -1,5 +1,5 @@
1
use {
2
- crate::app::{App, Focus, Mode, OndeApp, Screen, StatusTone},
2
+ crate::app::{App, FineTuneFocus, Focus, Mode, OndeApp, Screen, StatusTone},
3
crate::hf::CacheSource,
4
ratatui::{
5
Frame,
@@ -84,6 +84,7 @@ fn render_card(frame: &mut Frame, app: &App, area: Rect) {
84
Screen::Models => render_models(frame, app, inner),
85
Screen::Downloads => render_downloads(frame, app, inner),
86
Screen::ModelDetail => render_model_detail(frame, app, inner),
87
+ Screen::FineTune => render_finetune(frame, app, inner),
88
}
89
}
90
@@ -913,6 +914,277 @@ fn fmt_bytes(bytes: i64) -> String {
914
}
915
}
916
917
+fn render_finetune(frame: &mut Frame, app: &App, area: Rect) {
918
+ if app.finetune_running || app.finetune_progress.is_some() {
919
+ render_finetune_progress(frame, app, area);
920
+ } else {
921
+ render_finetune_form(frame, app, area);
922
+ }
923
+}
924
+
925
+fn render_finetune_form(frame: &mut Frame, app: &App, area: Rect) {
926
+ let rows = Layout::vertical([
927
+ Constraint::Length(1), // 0 heading
928
+ Constraint::Length(1), // 1 spacer
929
+ Constraint::Length(1), // 2 status
930
+ Constraint::Length(1), // 3 spacer
931
+ Constraint::Length(1), // 4 Model Dir label
932
+ Constraint::Length(3), // 5 Model Dir input
933
+ Constraint::Length(1), // 6 spacer
934
+ Constraint::Length(1), // 7 Data Path label
935
+ Constraint::Length(3), // 8 Data Path input
936
+ Constraint::Length(1), // 9 spacer
937
+ Constraint::Length(1), // 10 Rank label
938
+ Constraint::Length(3), // 11 Rank input
939
+ Constraint::Length(1), // 12 spacer
940
+ Constraint::Length(1), // 13 Epochs label
941
+ Constraint::Length(3), // 14 Epochs input
942
+ Constraint::Length(1), // 15 spacer
943
+ Constraint::Length(1), // 16 Learning Rate label
944
+ Constraint::Length(3), // 17 Learning Rate input
945
+ Constraint::Min(0), // 18 spacer
946
+ Constraint::Length(1), // 19 hint
947
+ ])
948
+ .split(area);
949
+
950
+ // Heading — show the model ID hint
951
+ let heading = if app.finetune_model_id.is_empty() {
952
+ "Fine-Tune".to_string()
953
+ } else {
954
+ format!("Fine-Tune — {}", app.finetune_model_id)
955
+ };
956
+ frame.render_widget(
957
+ Paragraph::new(Line::from(Span::styled(
958
+ heading,
959
+ Style::new().fg(C_INK).bold(),
960
+ ))),
961
+ rows[0],
962
+ );
963
+
964
+ render_status(frame, app, rows[2]);
965
+
966
+ // Model Dir
967
+ frame.render_widget(
968
+ Paragraph::new("Model Directory").style(Style::new().fg(C_MUTED)),
969
+ rows[4],
970
+ );
971
+ render_finetune_input(
972
+ frame,
973
+ &app.finetune_model_dir,
974
+ app.finetune_focus == FineTuneFocus::ModelDir,
975
+ rows[5],
976
+ );
977
+
978
+ // Data Path
979
+ frame.render_widget(
980
+ Paragraph::new("Training Data (JSONL)").style(Style::new().fg(C_MUTED)),
981
+ rows[7],
982
+ );
983
+ render_finetune_input(
984
+ frame,
985
+ &app.finetune_data_path,
986
+ app.finetune_focus == FineTuneFocus::DataPath,
987
+ rows[8],
988
+ );
989
+
990
+ // Rank
991
+ frame.render_widget(
992
+ Paragraph::new("LoRA Rank").style(Style::new().fg(C_MUTED)),
993
+ rows[10],
994
+ );
995
+ render_finetune_input(
996
+ frame,
997
+ &app.finetune_rank,
998
+ app.finetune_focus == FineTuneFocus::Rank,
999
+ rows[11],
1000
+ );
1001
+
1002
+ // Epochs
1003
+ frame.render_widget(
1004
+ Paragraph::new("Epochs").style(Style::new().fg(C_MUTED)),
1005
+ rows[13],
1006
+ );
1007
+ render_finetune_input(
1008
+ frame,
1009
+ &app.finetune_epochs,
1010
+ app.finetune_focus == FineTuneFocus::Epochs,
1011
+ rows[14],
1012
+ );
1013
+
1014
+ // Learning Rate
1015
+ frame.render_widget(
1016
+ Paragraph::new("Learning Rate").style(Style::new().fg(C_MUTED)),
1017
+ rows[16],
1018
+ );
1019
+ render_finetune_input(
1020
+ frame,
1021
+ &app.finetune_lr,
1022
+ app.finetune_focus == FineTuneFocus::Lr,
1023
+ rows[17],
1024
+ );
1025
+
1026
+ frame.render_widget(
1027
+ Paragraph::new("Enter · start Esc · back").style(Style::new().fg(C_MUTED)),
1028
+ rows[19],
1029
+ );
1030
+}
1031
+
1032
+fn render_finetune_input(frame: &mut Frame, value: &str, is_focused: bool, area: Rect) {
1033
+ let border_style = if is_focused {
1034
+ Style::new().fg(C_NEON)
1035
+ } else {
1036
+ Style::new().fg(C_LINE)
1037
+ };
1038
+
1039
+ let block = Block::new()
1040
+ .borders(Borders::ALL)
1041
+ .border_type(BorderType::Rounded)
1042
+ .border_style(border_style)
1043
+ .style(Style::new().bg(C_SURFACE_STRONG));
1044
+
1045
+ let inner = block.inner(area);
1046
+ frame.render_widget(block, area);
1047
+
1048
+ frame.render_widget(Paragraph::new(value).style(Style::new().fg(C_TEXT)), inner);
1049
+
1050
+ if is_focused {
1051
+ let cursor_x =
1052
+ (inner.x + value.chars().count() as u16).min(inner.x + inner.width.saturating_sub(1));
1053
+ frame.set_cursor_position((cursor_x, inner.y));
1054
+ }
1055
+}
1056
+
1057
+fn render_finetune_progress(frame: &mut Frame, app: &App, area: Rect) {
1058
+ let rows = Layout::vertical([
1059
+ Constraint::Length(1), // 0 heading
1060
+ Constraint::Length(1), // 1 spacer
1061
+ Constraint::Length(1), // 2 status
1062
+ Constraint::Length(1), // 3 spacer
1063
+ Constraint::Length(1), // 4 progress label
1064
+ Constraint::Length(1), // 5 progress detail
1065
+ Constraint::Length(1), // 6 spacer
1066
+ Constraint::Length(1), // 7 progress bar
1067
+ Constraint::Min(0), // 8 spacer
1068
+ Constraint::Length(1), // 9 hint
1069
+ ])
1070
+ .split(area);
1071
+
1072
+ // Heading
1073
+ let heading = if app.finetune_model_id.is_empty() {
1074
+ "Fine-Tune".to_string()
1075
+ } else {
1076
+ format!("Fine-Tune — {}", app.finetune_model_id)
1077
+ };
1078
+ frame.render_widget(
1079
+ Paragraph::new(Line::from(Span::styled(
1080
+ heading,
1081
+ Style::new().fg(C_INK).bold(),
1082
+ ))),
1083
+ rows[0],
1084
+ );
1085
+
1086
+ render_status(frame, app, rows[2]);
1087
+
1088
+ match &app.finetune_progress {
1089
+ Some(crate::finetune::FineTuneProgress::Validating) => {
1090
+ frame.render_widget(
1091
+ Paragraph::new("⠿ Validating model and data files…")
1092
+ .style(Style::new().fg(C_MUTED)),
1093
+ rows[4],
1094
+ );
1095
+ }
1096
+ Some(crate::finetune::FineTuneProgress::LoadingModel) => {
1097
+ frame.render_widget(
1098
+ Paragraph::new("⠿ Loading model weights…").style(Style::new().fg(C_MUTED)),
1099
+ rows[4],
1100
+ );
1101
+ }
1102
+ Some(crate::finetune::FineTuneProgress::Tokenizing { done, total }) => {
1103
+ frame.render_widget(
1104
+ Paragraph::new(format!("⠿ Tokenizing… {done}/{total}"))
1105
+ .style(Style::new().fg(C_MUTED)),
1106
+ rows[4],
1107
+ );
1108
+ }
1109
+ Some(crate::finetune::FineTuneProgress::Training {
1110
+ epoch,
1111
+ total_epochs,
1112
+ step,
1113
+ total_steps,
1114
+ loss,
1115
+ }) => {
1116
+ let detail =
1117
+ format!("epoch {epoch}/{total_epochs} step {step}/{total_steps} loss {loss:.3}");
1118
+ frame.render_widget(
1119
+ Paragraph::new("Training").style(Style::new().fg(C_NEON).bold()),
1120
+ rows[4],
1121
+ );
1122
+ frame.render_widget(
1123
+ Paragraph::new(detail).style(Style::new().fg(C_TEXT)),
1124
+ rows[5],
1125
+ );
1126
+
1127
+ // Progress bar
1128
+ let progress = if *total_steps > 0 {
1129
+ *step as f64 / *total_steps as f64
1130
+ } else {
1131
+ 0.0
1132
+ };
1133
+ let bar_width = area.width.saturating_sub(4) as usize;
1134
+ let filled = (progress * bar_width as f64) as usize;
1135
+ let empty = bar_width.saturating_sub(filled);
1136
+ let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
1137
+ frame.render_widget(Paragraph::new(bar).style(Style::new().fg(C_NEON)), rows[7]);
1138
+ }
1139
+ Some(crate::finetune::FineTuneProgress::Saving) => {
1140
+ frame.render_widget(
1141
+ Paragraph::new("⠿ Saving adapter weights…").style(Style::new().fg(C_MUTED)),
1142
+ rows[4],
1143
+ );
1144
+ }
1145
+ Some(crate::finetune::FineTuneProgress::Done { adapter_path }) => {
1146
+ frame.render_widget(
1147
+ Paragraph::new("✓ Done").style(Style::new().fg(C_NEON).bold()),
1148
+ rows[4],
1149
+ );
1150
+ frame.render_widget(
1151
+ Paragraph::new(adapter_path.to_string_lossy().to_string())
1152
+ .style(Style::new().fg(C_NEON)),
1153
+ rows[5],
1154
+ );
1155
+ }
1156
+ Some(crate::finetune::FineTuneProgress::Failed(msg)) => {
1157
+ frame.render_widget(
1158
+ Paragraph::new("✗ Failed").style(Style::new().fg(C_DANGER).bold()),
1159
+ rows[4],
1160
+ );
1161
+ frame.render_widget(
1162
+ Paragraph::new(msg.as_str())
1163
+ .style(Style::new().fg(C_DANGER))
1164
+ .wrap(Wrap { trim: true }),
1165
+ rows[5],
1166
+ );
1167
+ }
1168
+ None => {
1169
+ frame.render_widget(
1170
+ Paragraph::new("⠿ Starting…").style(Style::new().fg(C_MUTED)),
1171
+ rows[4],
1172
+ );
1173
+ }
1174
+ }
1175
+
1176
+ // Hint line
1177
+ let hint = if app.finetune_running {
1178
+ "Ctrl+C · quit"
1179
+ } else {
1180
+ "Esc · back"
1181
+ };
1182
+ frame.render_widget(
1183
+ Paragraph::new(hint).style(Style::new().fg(C_MUTED)),
1184
+ rows[9],
1185
+ );
1186
+}
1187
+
1188
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
1189
let keys: Vec<Span> = match app.screen {
1190
Screen::Auth => vec![
@@ -987,7 +1259,23 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
1259
Span::styled("Ctrl+C", Style::new().fg(C_NEON)),
1260
Span::styled(" · quit", Style::new().fg(C_MUTED)),
1261
],
1262
+ Screen::FineTune if app.finetune_running => vec![
1263
+ Span::styled("Ctrl+C", Style::new().fg(C_NEON)),
1264
+ Span::styled(" · quit", Style::new().fg(C_MUTED)),
1265
+ ],
1266
+ Screen::FineTune => vec![
1267
+ Span::styled("Tab", Style::new().fg(C_NEON)),
1268
+ Span::styled(" · next field ", Style::new().fg(C_MUTED)),
1269
+ Span::styled("Enter", Style::new().fg(C_NEON)),
1270
+ Span::styled(" · start ", Style::new().fg(C_MUTED)),
1271
+ Span::styled("Esc", Style::new().fg(C_NEON)),
1272
+ Span::styled(" · back ", Style::new().fg(C_MUTED)),
1273
+ Span::styled("Ctrl+C", Style::new().fg(C_NEON)),
1274
+ Span::styled(" · quit", Style::new().fg(C_MUTED)),
1275
+ ],
1276
Screen::ModelDetail => vec![
1277
+ Span::styled("f", Style::new().fg(C_NEON)),
1278
+ Span::styled(" · fine-tune ", Style::new().fg(C_MUTED)),
1279
Span::styled("Esc", Style::new().fg(C_NEON)),
1280
Span::styled(" · back ", Style::new().fg(C_MUTED)),
1281
Span::styled("Ctrl+C", Style::new().fg(C_NEON)),