sft-cli 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sft_cli-0.2.0 → sft_cli-0.2.1}/.github/workflows/ci.yml +4 -4
- {sft_cli-0.2.0 → sft_cli-0.2.1}/.github/workflows/publish.yml +1 -1
- sft_cli-0.2.1/LICENSE +21 -0
- sft_cli-0.2.1/PKG-INFO +155 -0
- sft_cli-0.2.1/README.md +121 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/pyproject.toml +2 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/__init__.py +1 -1
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/browser.py +101 -19
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/lora.py +10 -3
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/detect.py +10 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/svd.py +4 -2
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_detect.py +42 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_svd.py +32 -2
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_tui.py +150 -0
- sft_cli-0.2.0/PKG-INFO +0 -111
- sft_cli-0.2.0/README.md +0 -80
- sft_cli-0.2.0/scripts/create_test_file.py +0 -86
- sft_cli-0.2.0/scripts/generate_test_models.py +0 -306
- {sft_cli-0.2.0 → sft_cli-0.2.1}/.gitignore +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/.pre-commit-config.yaml +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/.python-version +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/AGENTS.md +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/docs/plans/2026-03-06-implementation-plan.md +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/docs/plans/2026-03-06-sft-toolkit-design.md +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/scripts/build_skill_reference.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/scripts/hatch_build_hook.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/cli.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/__init__.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/cast.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/cat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/check.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/convert.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/diff.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/info.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/ls.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/metadata.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/rename.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/skill.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/slice.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/split.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/stat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/strip.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/commands/tree.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/index.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/__init__.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/cast.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/cat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/check.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/convert.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/diff.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/info.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/__init__.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/add.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/compat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/convert.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/extract.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/info.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/merge.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/resize.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/lora/stack.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/ls.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/metadata.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/rename.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/slice.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/split.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/stat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/ops/tree.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/skill/REFERENCE.md +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/skill/SKILL.md +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/__init__.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/dtypes.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/formatting.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/glob.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/linking.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/output.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/src/sft/utils/tensor_io.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/__init__.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/conftest.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_build_hook.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_cast.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_cat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_check.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_cli.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_cli_contract.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_convert.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_diff.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_e2e_subprocess.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_info.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_add.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_compat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_convert.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_extract.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_info.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_merge.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_properties.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_resize.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_lora_stack.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_ls.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_metadata.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_perf.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_pipelines.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_properties.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_rename.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_skill_install.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_slice.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_split.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_stat.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_strip.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_tensor_io.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_tree.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/tests/test_utils.py +0 -0
- {sft_cli-0.2.0 → sft_cli-0.2.1}/uv.lock +0 -0
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
fail-fast: false
|
|
39
39
|
matrix:
|
|
40
40
|
os: [ubuntu-latest, macos-latest]
|
|
41
|
-
python-version: ["3.9", "3.
|
|
41
|
+
python-version: ["3.9", "3.13", "3.14"]
|
|
42
42
|
steps:
|
|
43
43
|
- uses: actions/checkout@v4
|
|
44
44
|
|
|
@@ -61,7 +61,7 @@ jobs:
|
|
|
61
61
|
# Long-running tests (subprocess E2E, perf smoke, build-hook) only need
|
|
62
62
|
# to pass on one cell — they exercise behavior that's OS- and version-
|
|
63
63
|
# independent.
|
|
64
|
-
name: slow tests (py3.
|
|
64
|
+
name: slow tests (py3.14 / ubuntu)
|
|
65
65
|
runs-on: ubuntu-latest
|
|
66
66
|
needs: test
|
|
67
67
|
steps:
|
|
@@ -71,7 +71,7 @@ jobs:
|
|
|
71
71
|
uses: astral-sh/setup-uv@v4
|
|
72
72
|
|
|
73
73
|
- name: Set up Python
|
|
74
|
-
run: uv python install 3.
|
|
74
|
+
run: uv python install 3.14
|
|
75
75
|
|
|
76
76
|
- name: Install dependencies
|
|
77
77
|
run: uv sync --dev
|
|
@@ -93,7 +93,7 @@ jobs:
|
|
|
93
93
|
uses: astral-sh/setup-uv@v4
|
|
94
94
|
|
|
95
95
|
- name: Set up Python
|
|
96
|
-
run: uv python install 3.
|
|
96
|
+
run: uv python install 3.14
|
|
97
97
|
|
|
98
98
|
- name: Build package
|
|
99
99
|
run: uv build
|
sft_cli-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matan Ben-Yosef
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sft_cli-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sft-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: An interactive terminal browser for .safetensors files
|
|
5
|
+
Project-URL: Homepage, https://github.com/matanby/sft-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/matanby/sft-cli
|
|
7
|
+
Author: Matan Ben-Yosef
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: browser,cli,machine-learning,safetensors,tui
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: ml-dtypes>=0.3
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Requires-Dist: safetensors>=0.4
|
|
29
|
+
Requires-Dist: textual>=0.40
|
|
30
|
+
Requires-Dist: typer>=0.9
|
|
31
|
+
Provides-Extra: torch
|
|
32
|
+
Requires-Dist: torch>=2.0; extra == 'torch'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# sft
|
|
36
|
+
|
|
37
|
+
> The Swiss army knife for `.safetensors` files.
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/sft-cli/)
|
|
40
|
+
[](https://pypi.org/project/sft-cli/)
|
|
41
|
+
[](https://github.com/matanby/sft-cli/actions)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
<img src="https://vhs.charm.sh/vhs-3nFXGLuvC5swABYCewgchD.gif" alt="sft demo">
|
|
45
|
+
|
|
46
|
+
`sft` is a single-binary CLI for inspecting, editing, and diffing `.safetensors` files — and an interactive terminal browser for poking around large checkpoints. Most commands read the file header only, so multi-gigabyte models open in milliseconds.
|
|
47
|
+
|
|
48
|
+
It also ships a [skill](#-ai-agents) that teaches AI coding agents (Claude Code, Cursor, Codex CLI) when to reach for it and how to parse the output.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv tool install sft-cli # recommended
|
|
54
|
+
pip install sft-cli # or pip
|
|
55
|
+
uv tool install 'sft-cli[torch]' # + .pt/.pth conversion
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🚀 Quick start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sft model.safetensors # open the interactive browser
|
|
62
|
+
sft info model.safetensors # one-shot summary
|
|
63
|
+
sft info model.safetensors --json # machine-readable
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The bare `sft <file>` form is a shortcut for `sft browse <file>`. Inside the browser: `↑↓` to navigate, `/` to filter, `L` on a LoRA file for LoRA Mode, `D` to diff against another file, `q` to quit.
|
|
67
|
+
|
|
68
|
+
## What it does
|
|
69
|
+
|
|
70
|
+
**Inspect** a file without loading it:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sft info model.safetensors # size, tensor count, dtypes, metadata
|
|
74
|
+
sft ls model.safetensors # flat list, sort/filter friendly
|
|
75
|
+
sft tree model.safetensors --depth=2 # hierarchical view
|
|
76
|
+
sft stat model.safetensors # per-tensor mean/std/min/max/sparsity
|
|
77
|
+
sft check model.safetensors # corruption + NaN/Inf scan
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Compare** two checkpoints:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
sft diff base.safetensors finetuned.safetensors --delta \
|
|
84
|
+
--include='**.self_attn.**' # cosine, L2, max-abs per tensor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Edit** without writing Python:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sft slice big.safetensors --include='**.weight' -o weights-only.safetensors
|
|
91
|
+
sft strip big.safetensors --exclude='*lora_*'
|
|
92
|
+
sft cast model.safetensors --dtype fp16
|
|
93
|
+
sft cat a.safetensors b.safetensors -o merged.safetensors
|
|
94
|
+
sft rename model.safetensors --sub 'model\.' 'backbone.'
|
|
95
|
+
sft split model.safetensors --max-size 4GB
|
|
96
|
+
sft convert pytorch_model.bin # → safetensors
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Every write command supports `--dry-run` and never overwrites the input — outputs default to `{stem}.{suffix}.safetensors`.
|
|
100
|
+
|
|
101
|
+
**Adapter workflows** (PEFT and Kohya):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
sft lora info adapter.safetensors # rank, alpha, target modules
|
|
105
|
+
sft lora svd adapter.safetensors # singular-value spectrum
|
|
106
|
+
sft lora compat base.safetensors adapter.safetensors
|
|
107
|
+
sft lora extract base.safetensors ft.safetensors --rank 16
|
|
108
|
+
sft lora resize adapter.safetensors --rank auto # per-pair adaptive rank
|
|
109
|
+
sft lora stack a.safetensors b.safetensors -a 0.7 -b 0.3
|
|
110
|
+
sft lora merge base.safetensors adapter.safetensors
|
|
111
|
+
sft lora convert adapter.safetensors --to peft # Kohya ↔ PEFT
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--rank auto` picks each pair's output rank from its singular-value spectrum (`ceil(stable_rank) + 1`), so over-parameterized pairs compress harder than rich ones. `auto+N` adds a safety margin.
|
|
115
|
+
|
|
116
|
+
## The browser
|
|
117
|
+
|
|
118
|
+
Press a key, get a result.
|
|
119
|
+
|
|
120
|
+
| Key | Action |
|
|
121
|
+
|:---:|---|
|
|
122
|
+
| `↑` `↓` | Navigate |
|
|
123
|
+
| `←` `→` | Collapse / expand tree |
|
|
124
|
+
| `Tab` | Switch between tree and table |
|
|
125
|
+
| `/` | Search / filter |
|
|
126
|
+
| `s` | Cycle sort |
|
|
127
|
+
| `Enter` | Tensor stats popup |
|
|
128
|
+
| `m` | File metadata |
|
|
129
|
+
| `c` | Cast file dtype |
|
|
130
|
+
| `L` | LoRA Mode (per-pair stats, SVD, compress) |
|
|
131
|
+
| `D` | Diff against another file |
|
|
132
|
+
| `:` | Command palette |
|
|
133
|
+
| `q` | Quit |
|
|
134
|
+
|
|
135
|
+
## 🤖 AI agents
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
sft skill install # auto-detects Claude / Cursor / Codex
|
|
139
|
+
sft skill status
|
|
140
|
+
sft skill uninstall
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The installer symlinks `sft`'s skill into your agent's well-known skills directory (`~/.claude/skills/sft`, `~/.cursor/skills/sft`, etc.), so it stays in sync when you `uv tool upgrade sft-cli`. Pass `--mode copy` for a frozen snapshot.
|
|
144
|
+
|
|
145
|
+
Every command supports `--json` for clean parsing:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
sft info model.safetensors --json | jq '.tensors'
|
|
149
|
+
sft lora info adapter.safetensors --json | jq '.rank'
|
|
150
|
+
sft stat model.safetensors --json --include='**.q_proj.*'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT — see [LICENSE](LICENSE).
|
sft_cli-0.2.1/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# sft
|
|
2
|
+
|
|
3
|
+
> The Swiss army knife for `.safetensors` files.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/sft-cli/)
|
|
6
|
+
[](https://pypi.org/project/sft-cli/)
|
|
7
|
+
[](https://github.com/matanby/sft-cli/actions)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
<img src="https://vhs.charm.sh/vhs-3nFXGLuvC5swABYCewgchD.gif" alt="sft demo">
|
|
11
|
+
|
|
12
|
+
`sft` is a single-binary CLI for inspecting, editing, and diffing `.safetensors` files — and an interactive terminal browser for poking around large checkpoints. Most commands read the file header only, so multi-gigabyte models open in milliseconds.
|
|
13
|
+
|
|
14
|
+
It also ships a [skill](#-ai-agents) that teaches AI coding agents (Claude Code, Cursor, Codex CLI) when to reach for it and how to parse the output.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install sft-cli # recommended
|
|
20
|
+
pip install sft-cli # or pip
|
|
21
|
+
uv tool install 'sft-cli[torch]' # + .pt/.pth conversion
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 🚀 Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sft model.safetensors # open the interactive browser
|
|
28
|
+
sft info model.safetensors # one-shot summary
|
|
29
|
+
sft info model.safetensors --json # machine-readable
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The bare `sft <file>` form is a shortcut for `sft browse <file>`. Inside the browser: `↑↓` to navigate, `/` to filter, `L` on a LoRA file for LoRA Mode, `D` to diff against another file, `q` to quit.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
**Inspect** a file without loading it:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
sft info model.safetensors # size, tensor count, dtypes, metadata
|
|
40
|
+
sft ls model.safetensors # flat list, sort/filter friendly
|
|
41
|
+
sft tree model.safetensors --depth=2 # hierarchical view
|
|
42
|
+
sft stat model.safetensors # per-tensor mean/std/min/max/sparsity
|
|
43
|
+
sft check model.safetensors # corruption + NaN/Inf scan
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Compare** two checkpoints:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
sft diff base.safetensors finetuned.safetensors --delta \
|
|
50
|
+
--include='**.self_attn.**' # cosine, L2, max-abs per tensor
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Edit** without writing Python:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sft slice big.safetensors --include='**.weight' -o weights-only.safetensors
|
|
57
|
+
sft strip big.safetensors --exclude='*lora_*'
|
|
58
|
+
sft cast model.safetensors --dtype fp16
|
|
59
|
+
sft cat a.safetensors b.safetensors -o merged.safetensors
|
|
60
|
+
sft rename model.safetensors --sub 'model\.' 'backbone.'
|
|
61
|
+
sft split model.safetensors --max-size 4GB
|
|
62
|
+
sft convert pytorch_model.bin # → safetensors
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every write command supports `--dry-run` and never overwrites the input — outputs default to `{stem}.{suffix}.safetensors`.
|
|
66
|
+
|
|
67
|
+
**Adapter workflows** (PEFT and Kohya):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
sft lora info adapter.safetensors # rank, alpha, target modules
|
|
71
|
+
sft lora svd adapter.safetensors # singular-value spectrum
|
|
72
|
+
sft lora compat base.safetensors adapter.safetensors
|
|
73
|
+
sft lora extract base.safetensors ft.safetensors --rank 16
|
|
74
|
+
sft lora resize adapter.safetensors --rank auto # per-pair adaptive rank
|
|
75
|
+
sft lora stack a.safetensors b.safetensors -a 0.7 -b 0.3
|
|
76
|
+
sft lora merge base.safetensors adapter.safetensors
|
|
77
|
+
sft lora convert adapter.safetensors --to peft # Kohya ↔ PEFT
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`--rank auto` picks each pair's output rank from its singular-value spectrum (`ceil(stable_rank) + 1`), so over-parameterized pairs compress harder than rich ones. `auto+N` adds a safety margin.
|
|
81
|
+
|
|
82
|
+
## The browser
|
|
83
|
+
|
|
84
|
+
Press a key, get a result.
|
|
85
|
+
|
|
86
|
+
| Key | Action |
|
|
87
|
+
|:---:|---|
|
|
88
|
+
| `↑` `↓` | Navigate |
|
|
89
|
+
| `←` `→` | Collapse / expand tree |
|
|
90
|
+
| `Tab` | Switch between tree and table |
|
|
91
|
+
| `/` | Search / filter |
|
|
92
|
+
| `s` | Cycle sort |
|
|
93
|
+
| `Enter` | Tensor stats popup |
|
|
94
|
+
| `m` | File metadata |
|
|
95
|
+
| `c` | Cast file dtype |
|
|
96
|
+
| `L` | LoRA Mode (per-pair stats, SVD, compress) |
|
|
97
|
+
| `D` | Diff against another file |
|
|
98
|
+
| `:` | Command palette |
|
|
99
|
+
| `q` | Quit |
|
|
100
|
+
|
|
101
|
+
## 🤖 AI agents
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
sft skill install # auto-detects Claude / Cursor / Codex
|
|
105
|
+
sft skill status
|
|
106
|
+
sft skill uninstall
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The installer symlinks `sft`'s skill into your agent's well-known skills directory (`~/.claude/skills/sft`, `~/.cursor/skills/sft`, etc.), so it stays in sync when you `uv tool upgrade sft-cli`. Pass `--mode copy` for a frozen snapshot.
|
|
110
|
+
|
|
111
|
+
Every command supports `--json` for clean parsing:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sft info model.safetensors --json | jq '.tensors'
|
|
115
|
+
sft lora info adapter.safetensors --json | jq '.rank'
|
|
116
|
+
sft stat model.safetensors --json --include='**.q_proj.*'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -20,6 +20,8 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.10",
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
23
25
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
26
|
"Topic :: Utilities",
|
|
25
27
|
]
|
|
@@ -740,6 +740,8 @@ class LoraSortMode(Enum):
|
|
|
740
740
|
RANK_ASC = "rank ↑"
|
|
741
741
|
EFF_RANK_DESC = "eff. rank ↓"
|
|
742
742
|
EFF_RANK_ASC = "eff. rank ↑"
|
|
743
|
+
SV95_DESC = "sv95 ↓"
|
|
744
|
+
SV95_ASC = "sv95 ↑"
|
|
743
745
|
NORM_A_DESC = "‖A‖ ↓"
|
|
744
746
|
NORM_A_ASC = "‖A‖ ↑"
|
|
745
747
|
NORM_B_DESC = "‖B‖ ↓"
|
|
@@ -753,6 +755,8 @@ LORA_SORT_ORDER = [
|
|
|
753
755
|
LoraSortMode.RANK_ASC,
|
|
754
756
|
LoraSortMode.EFF_RANK_DESC,
|
|
755
757
|
LoraSortMode.EFF_RANK_ASC,
|
|
758
|
+
LoraSortMode.SV95_DESC,
|
|
759
|
+
LoraSortMode.SV95_ASC,
|
|
756
760
|
LoraSortMode.NORM_A_DESC,
|
|
757
761
|
LoraSortMode.NORM_A_ASC,
|
|
758
762
|
LoraSortMode.NORM_B_DESC,
|
|
@@ -1295,6 +1299,7 @@ class LoraModeScreen(Screen):
|
|
|
1295
1299
|
self.lora_info = lora_info # only populated for PEFT
|
|
1296
1300
|
self._sort_idx = 0
|
|
1297
1301
|
self._stats: dict[str, dict[str, float]] = {}
|
|
1302
|
+
self._tensor_cache: dict | None = None
|
|
1298
1303
|
|
|
1299
1304
|
def compose(self) -> ComposeResult:
|
|
1300
1305
|
yield Footer()
|
|
@@ -1331,12 +1336,26 @@ class LoraModeScreen(Screen):
|
|
|
1331
1336
|
LoraSortMode.RANK_ASC: ("rank", " ↑"),
|
|
1332
1337
|
LoraSortMode.EFF_RANK_DESC: ("eff_rank", " ↓"),
|
|
1333
1338
|
LoraSortMode.EFF_RANK_ASC: ("eff_rank", " ↑"),
|
|
1339
|
+
LoraSortMode.SV95_DESC: ("sv95", " ↓"),
|
|
1340
|
+
LoraSortMode.SV95_ASC: ("sv95", " ↑"),
|
|
1334
1341
|
LoraSortMode.NORM_A_DESC: ("norm_a", " ↓"),
|
|
1335
1342
|
LoraSortMode.NORM_A_ASC: ("norm_a", " ↑"),
|
|
1336
1343
|
LoraSortMode.NORM_B_DESC: ("norm_b", " ↓"),
|
|
1337
1344
|
LoraSortMode.NORM_B_ASC: ("norm_b", " ↑"),
|
|
1338
1345
|
}
|
|
1339
1346
|
|
|
1347
|
+
_LORA_COLUMN_SORT: dict[str, tuple[LoraSortMode, LoraSortMode]] = {
|
|
1348
|
+
"module": (LoraSortMode.MODULE_ASC, LoraSortMode.MODULE_DESC),
|
|
1349
|
+
"rank": (LoraSortMode.RANK_ASC, LoraSortMode.RANK_DESC),
|
|
1350
|
+
"eff_rank": (LoraSortMode.EFF_RANK_ASC, LoraSortMode.EFF_RANK_DESC),
|
|
1351
|
+
"sv95": (LoraSortMode.SV95_ASC, LoraSortMode.SV95_DESC),
|
|
1352
|
+
"norm_a": (LoraSortMode.NORM_A_ASC, LoraSortMode.NORM_A_DESC),
|
|
1353
|
+
"norm_b": (LoraSortMode.NORM_B_ASC, LoraSortMode.NORM_B_DESC),
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
_SORT_MISSING_ASC = float("inf")
|
|
1357
|
+
_SORT_MISSING_DESC = float("-inf")
|
|
1358
|
+
|
|
1340
1359
|
def on_mount(self) -> None:
|
|
1341
1360
|
if self.lora_format == "peft" and self.lora_info:
|
|
1342
1361
|
table = self.query_one("#lora-table", DataTable)
|
|
@@ -1363,6 +1382,8 @@ class LoraModeScreen(Screen):
|
|
|
1363
1382
|
self._compute_stats()
|
|
1364
1383
|
self._update_sort_indicator()
|
|
1365
1384
|
|
|
1385
|
+
_STATS_REFRESH_EVERY = 64
|
|
1386
|
+
|
|
1366
1387
|
def _title_line(self) -> str:
|
|
1367
1388
|
fmt_label = self.lora_format.upper()
|
|
1368
1389
|
return (
|
|
@@ -1397,10 +1418,24 @@ class LoraModeScreen(Screen):
|
|
|
1397
1418
|
"…",
|
|
1398
1419
|
key=pair.module_key,
|
|
1399
1420
|
)
|
|
1421
|
+
self._sync_module_column_width(table)
|
|
1422
|
+
|
|
1423
|
+
def _sync_module_column_width(self, table: DataTable) -> None:
|
|
1424
|
+
if self.lora_info is None:
|
|
1425
|
+
return
|
|
1426
|
+
max_len = max(
|
|
1427
|
+
(len(self._short_module_name(p)) for p in self.lora_info.pairs),
|
|
1428
|
+
default=16,
|
|
1429
|
+
)
|
|
1430
|
+
col = table.columns.get("module")
|
|
1431
|
+
if col is not None:
|
|
1432
|
+
col.content_width = max(col.content_width, max_len)
|
|
1433
|
+
table.refresh(layout=True)
|
|
1400
1434
|
|
|
1401
1435
|
def _short_module_name(self, pair: LoRAPair) -> str:
|
|
1402
|
-
|
|
1403
|
-
|
|
1436
|
+
from sft.ops.lora.detect import format_lora_module_display
|
|
1437
|
+
|
|
1438
|
+
return format_lora_module_display(pair.module_key)
|
|
1404
1439
|
|
|
1405
1440
|
def _sorted_pairs(self) -> list[LoRAPair]:
|
|
1406
1441
|
if self.lora_info is None:
|
|
@@ -1408,8 +1443,9 @@ class LoraModeScreen(Screen):
|
|
|
1408
1443
|
mode = LORA_SORT_ORDER[self._sort_idx]
|
|
1409
1444
|
pairs = list(self.lora_info.pairs)
|
|
1410
1445
|
|
|
1411
|
-
def stat(name: str, key: str,
|
|
1412
|
-
|
|
1446
|
+
def stat(name: str, key: str, *, ascending: bool) -> float:
|
|
1447
|
+
missing = self._SORT_MISSING_ASC if ascending else self._SORT_MISSING_DESC
|
|
1448
|
+
return self._stats.get(name, {}).get(key, missing)
|
|
1413
1449
|
|
|
1414
1450
|
if mode == LoraSortMode.MODULE_ASC:
|
|
1415
1451
|
pairs.sort(key=lambda p: natural_sort_key(p.module_key))
|
|
@@ -1422,42 +1458,56 @@ class LoraModeScreen(Screen):
|
|
|
1422
1458
|
elif mode == LoraSortMode.EFF_RANK_DESC:
|
|
1423
1459
|
pairs.sort(
|
|
1424
1460
|
key=lambda p: (
|
|
1425
|
-
-stat(p.module_key, "eff_rank",
|
|
1461
|
+
-stat(p.module_key, "eff_rank", ascending=False),
|
|
1426
1462
|
natural_sort_key(p.module_key),
|
|
1427
1463
|
)
|
|
1428
1464
|
)
|
|
1429
1465
|
elif mode == LoraSortMode.EFF_RANK_ASC:
|
|
1430
1466
|
pairs.sort(
|
|
1431
1467
|
key=lambda p: (
|
|
1432
|
-
stat(p.module_key, "eff_rank",
|
|
1468
|
+
stat(p.module_key, "eff_rank", ascending=True),
|
|
1469
|
+
natural_sort_key(p.module_key),
|
|
1470
|
+
)
|
|
1471
|
+
)
|
|
1472
|
+
elif mode == LoraSortMode.SV95_DESC:
|
|
1473
|
+
pairs.sort(
|
|
1474
|
+
key=lambda p: (
|
|
1475
|
+
-stat(p.module_key, "sv95", ascending=False),
|
|
1476
|
+
natural_sort_key(p.module_key),
|
|
1477
|
+
)
|
|
1478
|
+
)
|
|
1479
|
+
elif mode == LoraSortMode.SV95_ASC:
|
|
1480
|
+
pairs.sort(
|
|
1481
|
+
key=lambda p: (
|
|
1482
|
+
stat(p.module_key, "sv95", ascending=True),
|
|
1433
1483
|
natural_sort_key(p.module_key),
|
|
1434
1484
|
)
|
|
1435
1485
|
)
|
|
1436
1486
|
elif mode == LoraSortMode.NORM_A_DESC:
|
|
1437
1487
|
pairs.sort(
|
|
1438
1488
|
key=lambda p: (
|
|
1439
|
-
-stat(p.module_key, "norm_a",
|
|
1489
|
+
-stat(p.module_key, "norm_a", ascending=False),
|
|
1440
1490
|
natural_sort_key(p.module_key),
|
|
1441
1491
|
)
|
|
1442
1492
|
)
|
|
1443
1493
|
elif mode == LoraSortMode.NORM_A_ASC:
|
|
1444
1494
|
pairs.sort(
|
|
1445
1495
|
key=lambda p: (
|
|
1446
|
-
stat(p.module_key, "norm_a",
|
|
1496
|
+
stat(p.module_key, "norm_a", ascending=True),
|
|
1447
1497
|
natural_sort_key(p.module_key),
|
|
1448
1498
|
)
|
|
1449
1499
|
)
|
|
1450
1500
|
elif mode == LoraSortMode.NORM_B_DESC:
|
|
1451
1501
|
pairs.sort(
|
|
1452
1502
|
key=lambda p: (
|
|
1453
|
-
-stat(p.module_key, "norm_b",
|
|
1503
|
+
-stat(p.module_key, "norm_b", ascending=False),
|
|
1454
1504
|
natural_sort_key(p.module_key),
|
|
1455
1505
|
)
|
|
1456
1506
|
)
|
|
1457
1507
|
elif mode == LoraSortMode.NORM_B_ASC:
|
|
1458
1508
|
pairs.sort(
|
|
1459
1509
|
key=lambda p: (
|
|
1460
|
-
stat(p.module_key, "norm_b",
|
|
1510
|
+
stat(p.module_key, "norm_b", ascending=True),
|
|
1461
1511
|
natural_sort_key(p.module_key),
|
|
1462
1512
|
)
|
|
1463
1513
|
)
|
|
@@ -1509,6 +1559,7 @@ class LoraModeScreen(Screen):
|
|
|
1509
1559
|
new_cursor = i
|
|
1510
1560
|
if table.row_count > 0:
|
|
1511
1561
|
table.move_cursor(row=new_cursor)
|
|
1562
|
+
self._sync_module_column_width(table)
|
|
1512
1563
|
self._update_sort_indicator()
|
|
1513
1564
|
|
|
1514
1565
|
@staticmethod
|
|
@@ -1541,17 +1592,20 @@ class LoraModeScreen(Screen):
|
|
|
1541
1592
|
)
|
|
1542
1593
|
return
|
|
1543
1594
|
|
|
1544
|
-
|
|
1595
|
+
self._tensor_cache = tensors
|
|
1596
|
+
total = len(self.lora_info.pairs)
|
|
1597
|
+
|
|
1598
|
+
for i, pair in enumerate(self.lora_info.pairs):
|
|
1545
1599
|
try:
|
|
1546
1600
|
a = tensors[pair.lora_a_name]
|
|
1547
1601
|
b = tensors[pair.lora_b_name]
|
|
1548
1602
|
|
|
1549
1603
|
(s,) = _qr_svd(a, b, compute_uv=False)
|
|
1550
1604
|
s_sq = s**2
|
|
1551
|
-
|
|
1552
|
-
if
|
|
1553
|
-
eff_rank =
|
|
1554
|
-
cumvar = np.cumsum(s_sq) /
|
|
1605
|
+
total_energy = float(s_sq.sum())
|
|
1606
|
+
if total_energy > 0:
|
|
1607
|
+
eff_rank = total_energy / float(s_sq.max())
|
|
1608
|
+
cumvar = np.cumsum(s_sq) / total_energy
|
|
1555
1609
|
sv95 = int(np.searchsorted(cumvar, 0.95)) + 1
|
|
1556
1610
|
else:
|
|
1557
1611
|
eff_rank = 0.0
|
|
@@ -1569,19 +1623,43 @@ class LoraModeScreen(Screen):
|
|
|
1569
1623
|
stats = {}
|
|
1570
1624
|
|
|
1571
1625
|
self._stats[pair.module_key] = stats
|
|
1572
|
-
self.
|
|
1626
|
+
if (i + 1) % self._STATS_REFRESH_EVERY == 0 or (i + 1) == total:
|
|
1627
|
+
self.app.call_from_thread(self._refresh_table)
|
|
1573
1628
|
|
|
1574
1629
|
# --- Actions ---
|
|
1575
1630
|
|
|
1576
1631
|
def action_exit_mode(self) -> None:
|
|
1577
1632
|
self.app.pop_screen()
|
|
1578
1633
|
|
|
1634
|
+
def _set_sort_mode(self, mode: LoraSortMode) -> None:
|
|
1635
|
+
if self.lora_format != "peft":
|
|
1636
|
+
return
|
|
1637
|
+
self._sort_idx = LORA_SORT_ORDER.index(mode)
|
|
1638
|
+
self._refresh_table()
|
|
1639
|
+
|
|
1579
1640
|
def action_cycle_sort(self) -> None:
|
|
1580
1641
|
if self.lora_format != "peft":
|
|
1581
1642
|
return
|
|
1582
1643
|
self._sort_idx = (self._sort_idx + 1) % len(LORA_SORT_ORDER)
|
|
1583
1644
|
self._refresh_table()
|
|
1584
1645
|
|
|
1646
|
+
def on_data_table_header_selected(self, event: DataTable.HeaderSelected) -> None:
|
|
1647
|
+
"""Click a column header to toggle asc/desc for that metric."""
|
|
1648
|
+
if self.lora_format != "peft" or event.control.id != "lora-table":
|
|
1649
|
+
return
|
|
1650
|
+
col_key = event.column_key.value
|
|
1651
|
+
modes = self._LORA_COLUMN_SORT.get(col_key)
|
|
1652
|
+
if modes is None:
|
|
1653
|
+
return
|
|
1654
|
+
asc, desc = modes
|
|
1655
|
+
current = LORA_SORT_ORDER[self._sort_idx]
|
|
1656
|
+
if current == asc:
|
|
1657
|
+
self._set_sort_mode(desc)
|
|
1658
|
+
elif current == desc:
|
|
1659
|
+
self._set_sort_mode(asc)
|
|
1660
|
+
else:
|
|
1661
|
+
self._set_sort_mode(desc if col_key != "module" else asc)
|
|
1662
|
+
|
|
1585
1663
|
def on_data_table_row_selected(self, _event: DataTable.RowSelected) -> None:
|
|
1586
1664
|
self.action_show_spectrum()
|
|
1587
1665
|
|
|
@@ -1785,8 +1863,9 @@ class LoraInfoScreen(ModalScreen):
|
|
|
1785
1863
|
|
|
1786
1864
|
@staticmethod
|
|
1787
1865
|
def _short_pair_name(pair: LoRAPair) -> str:
|
|
1788
|
-
|
|
1789
|
-
|
|
1866
|
+
from sft.ops.lora.detect import format_lora_module_display
|
|
1867
|
+
|
|
1868
|
+
return format_lora_module_display(pair.module_key)
|
|
1790
1869
|
|
|
1791
1870
|
|
|
1792
1871
|
class LoraResizePromptScreen(ModalScreen):
|
|
@@ -1961,8 +2040,11 @@ class SvdSpectrumScreen(ModalScreen):
|
|
|
1961
2040
|
self.sv = [float(v) for v in singular_values]
|
|
1962
2041
|
|
|
1963
2042
|
def compose(self) -> ComposeResult:
|
|
2043
|
+
from sft.ops.lora.detect import format_lora_module_display
|
|
2044
|
+
|
|
2045
|
+
module_label = format_lora_module_display(self.pair.module_key)
|
|
1964
2046
|
with Container(id="svd-container"):
|
|
1965
|
-
yield Label(f"SVD Spectrum — {
|
|
2047
|
+
yield Label(f"SVD Spectrum — {module_label}", id="svd-title")
|
|
1966
2048
|
yield Static(
|
|
1967
2049
|
f"[dim]Module:[/dim] {self.pair.module_key}\n"
|
|
1968
2050
|
f"[dim]Rank:[/dim] {self.pair.rank} "
|
|
@@ -115,6 +115,7 @@ def lora_info_cmd(
|
|
|
115
115
|
"""
|
|
116
116
|
file = validate_safetensors(file)
|
|
117
117
|
|
|
118
|
+
from sft.ops.lora.detect import format_lora_module_display
|
|
118
119
|
from sft.ops.lora.info import lora_info
|
|
119
120
|
|
|
120
121
|
try:
|
|
@@ -153,7 +154,7 @@ def lora_info_cmd(
|
|
|
153
154
|
|
|
154
155
|
typer.echo("Pairs:")
|
|
155
156
|
for pair in info.pairs:
|
|
156
|
-
module_short = pair.module_key
|
|
157
|
+
module_short = format_lora_module_display(pair.module_key)
|
|
157
158
|
a_shape = "x".join(str(d) for d in pair.lora_a_shape)
|
|
158
159
|
b_shape = "x".join(str(d) for d in pair.lora_b_shape)
|
|
159
160
|
typer.echo(
|
|
@@ -516,6 +517,7 @@ def lora_svd_cmd(
|
|
|
516
517
|
"threshold": analysis.threshold,
|
|
517
518
|
"modules": [
|
|
518
519
|
{
|
|
520
|
+
"module_key": m.module_key,
|
|
519
521
|
"module": m.module,
|
|
520
522
|
"rank": m.rank,
|
|
521
523
|
"sv_90": m.sv_90,
|
|
@@ -530,11 +532,16 @@ def lora_svd_cmd(
|
|
|
530
532
|
typer.echo(json.dumps(data, indent=2))
|
|
531
533
|
return
|
|
532
534
|
|
|
533
|
-
|
|
535
|
+
col_width = max(16, *(len(m.module) for m in analysis.modules))
|
|
536
|
+
header = (
|
|
537
|
+
f"{'Module':<{col_width}}{'Rank':>6}{'SV 90%':>10}{'SV 95%':>10}"
|
|
538
|
+
f"{'SV 99%':>10}{'Suggested rank':>16}"
|
|
539
|
+
)
|
|
534
540
|
typer.echo(header)
|
|
535
541
|
for m in analysis.modules:
|
|
536
542
|
typer.echo(
|
|
537
|
-
f"{m.module:<
|
|
543
|
+
f"{m.module:<{col_width}}{m.rank:>6}{m.sv_90:>10}{m.sv_95:>10}"
|
|
544
|
+
f"{m.sv_99:>10}{m.suggested_rank:>16}"
|
|
538
545
|
)
|
|
539
546
|
|
|
540
547
|
typer.echo()
|