@ondeinference / cli / commits / 4ce17f2

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)),