mod-trace 0.2.0__tar.gz → 0.3.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.
- {mod_trace-0.2.0 → mod_trace-0.3.1}/.gitignore +2 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/Cargo.lock +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.1}/Cargo.toml +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.1}/PKG-INFO +77 -57
- {mod_trace-0.2.0 → mod_trace-0.3.1}/README.md +76 -56
- mod_trace-0.3.1/docs/tensor-lab.md +66 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/pyproject.toml +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/main.rs +316 -13
- mod_trace-0.2.0/.claude/scheduled_tasks.lock +0 -1
- mod_trace-0.2.0/mt-demo/make_onnx_pair.py +0 -27
- mod_trace-0.2.0/mt-demo/model_a.onnx +0 -0
- mod_trace-0.2.0/mt-demo/model_b.onnx +0 -0
- mod_trace-0.2.0/mt-demo/model_v1.onnx +0 -0
- mod_trace-0.2.0/mt-demo/model_v2.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/.github/workflows/release.yml +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/LICENSE +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/benchmarks/tiny_pytorch.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/docs/ARCHITECTURE.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/docs/REAL_MODELS.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/broken_shape.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/lightgbm/README.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/lightgbm/clf_v1.txt +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/lightgbm/clf_v2.txt +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/lightgbm/generate_demo_models.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/make_sample_catboost.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/mlp.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/README.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/generate_demo_models.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/mlp_retrain_a.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/mlp_retrain_b.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/mlp_v1.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/onnx/mlp_v2.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/tiny_attention.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/examples/tiny_attention_plan.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/catboost_deep_diff.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/catboost_explain.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/cbm.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/demo.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/explain.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/lgbm.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/model.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/onnx.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.1}/src/tensor.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mod-trace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -27,18 +27,14 @@ What is inside this model file?
|
|
|
27
27
|
|
|
28
28
|
It can inspect real artifacts such as CatBoost `.cbm` files, LightGBM `.txt`/`.lgb` text models, and ONNX `.onnx` graphs, then report structure, size, parameters, operator mix, rough inference cost, and changes between versions. CatBoost, LightGBM, and ONNX are all read natively — no Python, framework, or runtime needed (CatBoost `--deep` is the one optional exception).
|
|
29
29
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
```sql
|
|
33
|
-
EXPLAIN ANALYZE SELECT ...
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
becomes:
|
|
30
|
+
The most useful command is `explain-diff`, which says in plain English what changed between two model versions:
|
|
37
31
|
|
|
38
32
|
```sh
|
|
39
|
-
mod-trace
|
|
33
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
40
34
|
```
|
|
41
35
|
|
|
36
|
+
(A secondary "tensor lab" for handcrafted JSON plans lives in [docs/tensor-lab.md](docs/tensor-lab.md).)
|
|
37
|
+
|
|
42
38
|
## Core Commands
|
|
43
39
|
|
|
44
40
|
```sh
|
|
@@ -73,7 +69,10 @@ mod-trace explain model.onnx
|
|
|
73
69
|
mod-trace diff old_model.cbm new_model.cbm
|
|
74
70
|
mod-trace diff --json old_model.cbm new_model.cbm
|
|
75
71
|
mod-trace diff --deep old_model.cbm new_model.cbm
|
|
72
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
76
73
|
mod-trace check --max-size-growth 20% --fail-on-feature-change old_model.cbm new_model.cbm
|
|
74
|
+
mod-trace inspect model.lgb # LightGBM (.lgb/.txt), read natively
|
|
75
|
+
mod-trace diff old.lgb new.lgb
|
|
77
76
|
```
|
|
78
77
|
|
|
79
78
|
## Why This Exists
|
|
@@ -160,10 +159,54 @@ cargo run -- check path/to/old_model.cbm path/to/new_model.cbm \
|
|
|
160
159
|
cargo run -- check path/to/old_model.onnx path/to/new_model.onnx \
|
|
161
160
|
--max-size-growth 20% \
|
|
162
161
|
--max-ops-growth 25% \
|
|
162
|
+
--max-parameter-growth 30% \
|
|
163
163
|
--fail-on-new-op
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
Check rules:
|
|
167
|
+
|
|
168
|
+
| Rule | Applies to | Fails when |
|
|
169
|
+
|------|------------|-----------|
|
|
170
|
+
| `--max-size-growth <pct>` | all | file size grows more than `<pct>` |
|
|
171
|
+
| `--max-parameter-growth <pct>` | ONNX | parameter count grows more than `<pct>` |
|
|
172
|
+
| `--max-ops-growth <pct>` | ONNX | estimated op count grows more than `<pct>` |
|
|
173
|
+
| `--fail-on-new-op` | ONNX | a new operator type appears |
|
|
174
|
+
| `--fail-on-feature-change` | CatBoost, LightGBM | feature names change |
|
|
175
|
+
| `--fail-on-training-config-change` | CatBoost, LightGBM | objective/learning rate/etc. change |
|
|
176
|
+
|
|
177
|
+
`check` prints a short PASS/FAIL report and exits nonzero when a rule fails. Any number of rules can be combined; any one failing fails the whole check.
|
|
178
|
+
|
|
179
|
+
## Explain Diff
|
|
180
|
+
|
|
181
|
+
`explain-diff` is the plain-English version of `diff` — it reports what actually changed between two model versions, not just raw numbers:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
Model Change Explanation
|
|
189
|
+
------------------------
|
|
190
|
+
Type: ONNX
|
|
191
|
+
Old: old_model.onnx
|
|
192
|
+
New: new_model.onnx
|
|
193
|
+
|
|
194
|
+
Architecture:
|
|
195
|
+
Attention layers: 12 -> 24
|
|
196
|
+
Hidden size: 768 -> 1024
|
|
197
|
+
Parameters: 110.0M -> 220.0M (+100%)
|
|
198
|
+
Nodes: 420 -> 820 (+95%)
|
|
199
|
+
|
|
200
|
+
Estimated inference cost (static op proxy): +94%
|
|
201
|
+
|
|
202
|
+
New operators introduced:
|
|
203
|
+
LayerNormalization
|
|
204
|
+
|
|
205
|
+
Summary:
|
|
206
|
+
Grew from ~12 to ~24 attention layers; parameters +100%, estimated cost +94%.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Works for ONNX, CatBoost, and LightGBM (tree models report trees / leaves / learned-state instead of attention layers).
|
|
167
210
|
|
|
168
211
|
## CatBoost
|
|
169
212
|
|
|
@@ -457,61 +500,38 @@ cargo run -- inspect models/tiny-distilbert-base-cased/model_fixed.onnx
|
|
|
457
500
|
|
|
458
501
|
Fixed shapes such as `[1, 8]` produce better numeric estimates than symbolic shapes such as `[batch, sequence]`.
|
|
459
502
|
|
|
460
|
-
|
|
503
|
+
### Exporting any PyTorch model to ONNX
|
|
461
504
|
|
|
462
|
-
The
|
|
505
|
+
mod-trace does not read native PyTorch `.pt`/`.pth` files (those are Python pickles / TorchScript archives). The supported path is to export to ONNX, which is the usual serving format anyway. For a plain `nn.Module` the export is a single call:
|
|
463
506
|
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
507
|
+
```python
|
|
508
|
+
import torch
|
|
509
|
+
|
|
510
|
+
model.eval()
|
|
511
|
+
dummy = torch.randn(1, n_features) # one example input with the right shape
|
|
512
|
+
torch.onnx.export(
|
|
513
|
+
model,
|
|
514
|
+
dummy,
|
|
515
|
+
"model.onnx",
|
|
516
|
+
input_names=["input"],
|
|
517
|
+
output_names=["output"],
|
|
518
|
+
opset_version=18,
|
|
519
|
+
dynamo=True, # optional; the modern exporter
|
|
520
|
+
)
|
|
469
521
|
```
|
|
470
522
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
{
|
|
475
|
-
"layers": [
|
|
476
|
-
{
|
|
477
|
-
"type": "self_attention",
|
|
478
|
-
"tokens": 3,
|
|
479
|
-
"head_dim": 4,
|
|
480
|
-
"value_dim": 4
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
"type": "linear",
|
|
484
|
-
"in": 4,
|
|
485
|
-
"out": 2
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
"type": "softmax"
|
|
489
|
-
}
|
|
490
|
-
]
|
|
491
|
-
}
|
|
523
|
+
```sh
|
|
524
|
+
mod-trace inspect model.onnx
|
|
525
|
+
mod-trace diff old_model.onnx new_model.onnx
|
|
492
526
|
```
|
|
493
527
|
|
|
494
|
-
`
|
|
528
|
+
Use fixed input shapes (e.g. `torch.randn(1, n_features)`) rather than dynamic axes for better cost estimates. Once exported, ONNX is read natively — no PyTorch, ONNX Runtime, or other framework is needed to inspect it.
|
|
495
529
|
|
|
496
|
-
|
|
497
|
-
Why is attention expensive?
|
|
498
|
-
---------------------------
|
|
499
|
-
Attention layer: single_head_attention
|
|
500
|
-
432 ops
|
|
501
|
-
|
|
502
|
-
Breakdown:
|
|
503
|
-
Q projection 96 ops
|
|
504
|
-
K projection 96 ops
|
|
505
|
-
V projection 96 ops
|
|
506
|
-
Q @ K^T 72 ops
|
|
507
|
-
attention @ V 72 ops
|
|
508
|
-
|
|
509
|
-
Explanation:
|
|
510
|
-
Every token must compare itself against every other token.
|
|
511
|
-
The score and value-mixing terms grow roughly with tokens^2.
|
|
512
|
-
```
|
|
530
|
+
## Tensor Lab
|
|
513
531
|
|
|
514
|
-
|
|
532
|
+
A secondary lab for explaining transformer internals on small handcrafted JSON
|
|
533
|
+
plans (`trace`, `compare`, `why`, `validate`, `quiz`, `demo`). It is not the
|
|
534
|
+
primary product surface. See [docs/tensor-lab.md](docs/tensor-lab.md).
|
|
515
535
|
|
|
516
536
|
## What It Does Not Do
|
|
517
537
|
|
|
@@ -10,18 +10,14 @@ What is inside this model file?
|
|
|
10
10
|
|
|
11
11
|
It can inspect real artifacts such as CatBoost `.cbm` files, LightGBM `.txt`/`.lgb` text models, and ONNX `.onnx` graphs, then report structure, size, parameters, operator mix, rough inference cost, and changes between versions. CatBoost, LightGBM, and ONNX are all read natively — no Python, framework, or runtime needed (CatBoost `--deep` is the one optional exception).
|
|
12
12
|
|
|
13
|
-
The
|
|
14
|
-
|
|
15
|
-
```sql
|
|
16
|
-
EXPLAIN ANALYZE SELECT ...
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
becomes:
|
|
13
|
+
The most useful command is `explain-diff`, which says in plain English what changed between two model versions:
|
|
20
14
|
|
|
21
15
|
```sh
|
|
22
|
-
mod-trace
|
|
16
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
23
17
|
```
|
|
24
18
|
|
|
19
|
+
(A secondary "tensor lab" for handcrafted JSON plans lives in [docs/tensor-lab.md](docs/tensor-lab.md).)
|
|
20
|
+
|
|
25
21
|
## Core Commands
|
|
26
22
|
|
|
27
23
|
```sh
|
|
@@ -56,7 +52,10 @@ mod-trace explain model.onnx
|
|
|
56
52
|
mod-trace diff old_model.cbm new_model.cbm
|
|
57
53
|
mod-trace diff --json old_model.cbm new_model.cbm
|
|
58
54
|
mod-trace diff --deep old_model.cbm new_model.cbm
|
|
55
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
59
56
|
mod-trace check --max-size-growth 20% --fail-on-feature-change old_model.cbm new_model.cbm
|
|
57
|
+
mod-trace inspect model.lgb # LightGBM (.lgb/.txt), read natively
|
|
58
|
+
mod-trace diff old.lgb new.lgb
|
|
60
59
|
```
|
|
61
60
|
|
|
62
61
|
## Why This Exists
|
|
@@ -143,10 +142,54 @@ cargo run -- check path/to/old_model.cbm path/to/new_model.cbm \
|
|
|
143
142
|
cargo run -- check path/to/old_model.onnx path/to/new_model.onnx \
|
|
144
143
|
--max-size-growth 20% \
|
|
145
144
|
--max-ops-growth 25% \
|
|
145
|
+
--max-parameter-growth 30% \
|
|
146
146
|
--fail-on-new-op
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
Check rules:
|
|
150
|
+
|
|
151
|
+
| Rule | Applies to | Fails when |
|
|
152
|
+
|------|------------|-----------|
|
|
153
|
+
| `--max-size-growth <pct>` | all | file size grows more than `<pct>` |
|
|
154
|
+
| `--max-parameter-growth <pct>` | ONNX | parameter count grows more than `<pct>` |
|
|
155
|
+
| `--max-ops-growth <pct>` | ONNX | estimated op count grows more than `<pct>` |
|
|
156
|
+
| `--fail-on-new-op` | ONNX | a new operator type appears |
|
|
157
|
+
| `--fail-on-feature-change` | CatBoost, LightGBM | feature names change |
|
|
158
|
+
| `--fail-on-training-config-change` | CatBoost, LightGBM | objective/learning rate/etc. change |
|
|
159
|
+
|
|
160
|
+
`check` prints a short PASS/FAIL report and exits nonzero when a rule fails. Any number of rules can be combined; any one failing fails the whole check.
|
|
161
|
+
|
|
162
|
+
## Explain Diff
|
|
163
|
+
|
|
164
|
+
`explain-diff` is the plain-English version of `diff` — it reports what actually changed between two model versions, not just raw numbers:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
mod-trace explain-diff old_model.onnx new_model.onnx
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Model Change Explanation
|
|
172
|
+
------------------------
|
|
173
|
+
Type: ONNX
|
|
174
|
+
Old: old_model.onnx
|
|
175
|
+
New: new_model.onnx
|
|
176
|
+
|
|
177
|
+
Architecture:
|
|
178
|
+
Attention layers: 12 -> 24
|
|
179
|
+
Hidden size: 768 -> 1024
|
|
180
|
+
Parameters: 110.0M -> 220.0M (+100%)
|
|
181
|
+
Nodes: 420 -> 820 (+95%)
|
|
182
|
+
|
|
183
|
+
Estimated inference cost (static op proxy): +94%
|
|
184
|
+
|
|
185
|
+
New operators introduced:
|
|
186
|
+
LayerNormalization
|
|
187
|
+
|
|
188
|
+
Summary:
|
|
189
|
+
Grew from ~12 to ~24 attention layers; parameters +100%, estimated cost +94%.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Works for ONNX, CatBoost, and LightGBM (tree models report trees / leaves / learned-state instead of attention layers).
|
|
150
193
|
|
|
151
194
|
## CatBoost
|
|
152
195
|
|
|
@@ -440,61 +483,38 @@ cargo run -- inspect models/tiny-distilbert-base-cased/model_fixed.onnx
|
|
|
440
483
|
|
|
441
484
|
Fixed shapes such as `[1, 8]` produce better numeric estimates than symbolic shapes such as `[batch, sequence]`.
|
|
442
485
|
|
|
443
|
-
|
|
486
|
+
### Exporting any PyTorch model to ONNX
|
|
444
487
|
|
|
445
|
-
The
|
|
488
|
+
mod-trace does not read native PyTorch `.pt`/`.pth` files (those are Python pickles / TorchScript archives). The supported path is to export to ONNX, which is the usual serving format anyway. For a plain `nn.Module` the export is a single call:
|
|
446
489
|
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
490
|
+
```python
|
|
491
|
+
import torch
|
|
492
|
+
|
|
493
|
+
model.eval()
|
|
494
|
+
dummy = torch.randn(1, n_features) # one example input with the right shape
|
|
495
|
+
torch.onnx.export(
|
|
496
|
+
model,
|
|
497
|
+
dummy,
|
|
498
|
+
"model.onnx",
|
|
499
|
+
input_names=["input"],
|
|
500
|
+
output_names=["output"],
|
|
501
|
+
opset_version=18,
|
|
502
|
+
dynamo=True, # optional; the modern exporter
|
|
503
|
+
)
|
|
452
504
|
```
|
|
453
505
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
{
|
|
458
|
-
"layers": [
|
|
459
|
-
{
|
|
460
|
-
"type": "self_attention",
|
|
461
|
-
"tokens": 3,
|
|
462
|
-
"head_dim": 4,
|
|
463
|
-
"value_dim": 4
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
"type": "linear",
|
|
467
|
-
"in": 4,
|
|
468
|
-
"out": 2
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
"type": "softmax"
|
|
472
|
-
}
|
|
473
|
-
]
|
|
474
|
-
}
|
|
506
|
+
```sh
|
|
507
|
+
mod-trace inspect model.onnx
|
|
508
|
+
mod-trace diff old_model.onnx new_model.onnx
|
|
475
509
|
```
|
|
476
510
|
|
|
477
|
-
`
|
|
511
|
+
Use fixed input shapes (e.g. `torch.randn(1, n_features)`) rather than dynamic axes for better cost estimates. Once exported, ONNX is read natively — no PyTorch, ONNX Runtime, or other framework is needed to inspect it.
|
|
478
512
|
|
|
479
|
-
|
|
480
|
-
Why is attention expensive?
|
|
481
|
-
---------------------------
|
|
482
|
-
Attention layer: single_head_attention
|
|
483
|
-
432 ops
|
|
484
|
-
|
|
485
|
-
Breakdown:
|
|
486
|
-
Q projection 96 ops
|
|
487
|
-
K projection 96 ops
|
|
488
|
-
V projection 96 ops
|
|
489
|
-
Q @ K^T 72 ops
|
|
490
|
-
attention @ V 72 ops
|
|
491
|
-
|
|
492
|
-
Explanation:
|
|
493
|
-
Every token must compare itself against every other token.
|
|
494
|
-
The score and value-mixing terms grow roughly with tokens^2.
|
|
495
|
-
```
|
|
513
|
+
## Tensor Lab
|
|
496
514
|
|
|
497
|
-
|
|
515
|
+
A secondary lab for explaining transformer internals on small handcrafted JSON
|
|
516
|
+
plans (`trace`, `compare`, `why`, `validate`, `quiz`, `demo`). It is not the
|
|
517
|
+
primary product surface. See [docs/tensor-lab.md](docs/tensor-lab.md).
|
|
498
518
|
|
|
499
519
|
## What It Does Not Do
|
|
500
520
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Tensor Lab (secondary)
|
|
2
|
+
|
|
3
|
+
The original tensor-analysis MVP. It is **not** the primary product surface —
|
|
4
|
+
mod-trace's main job is inspecting real model artifacts (CatBoost, LightGBM,
|
|
5
|
+
ONNX). The tensor lab is kept for demos and for explaining transformer
|
|
6
|
+
internals on small handcrafted JSON plans, in the spirit of `EXPLAIN ANALYZE`:
|
|
7
|
+
|
|
8
|
+
```sql
|
|
9
|
+
EXPLAIN ANALYZE SELECT ...
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
becomes:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
mod-trace trace examples/tiny_attention_plan.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
mod-trace trace examples/tiny_attention_plan.json # summarize a plan
|
|
22
|
+
mod-trace compare examples/tiny_attention_plan.json # compare layers
|
|
23
|
+
mod-trace why examples/tiny_attention_plan.json # explain attention cost
|
|
24
|
+
mod-trace validate examples/broken_shape.json # shape validation
|
|
25
|
+
mod-trace tensor-inspect examples/mlp.json
|
|
26
|
+
mod-trace quiz examples/mlp.json
|
|
27
|
+
mod-trace demo attention
|
|
28
|
+
mod-trace explain <topic> [--step] [--shapes|--memory|--math|--compare] [--quiz]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
(With a source checkout, replace `mod-trace` with `cargo run --`.)
|
|
32
|
+
|
|
33
|
+
## Example plan
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"layers": [
|
|
38
|
+
{ "type": "self_attention", "tokens": 3, "head_dim": 4, "value_dim": 4 },
|
|
39
|
+
{ "type": "linear", "in": 4, "out": 2 },
|
|
40
|
+
{ "type": "softmax" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## `why` explains the attention cost
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
Why is attention expensive?
|
|
49
|
+
---------------------------
|
|
50
|
+
Attention layer: single_head_attention
|
|
51
|
+
432 ops
|
|
52
|
+
|
|
53
|
+
Breakdown:
|
|
54
|
+
Q projection 96 ops
|
|
55
|
+
K projection 96 ops
|
|
56
|
+
V projection 96 ops
|
|
57
|
+
Q @ K^T 72 ops
|
|
58
|
+
attention @ V 72 ops
|
|
59
|
+
|
|
60
|
+
Explanation:
|
|
61
|
+
Every token must compare itself against every other token.
|
|
62
|
+
The score and value-mixing terms grow roughly with tokens^2.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This is useful for demos and for explaining transformer internals, but it is no
|
|
66
|
+
longer the primary product surface.
|
|
@@ -40,6 +40,7 @@ fn run() -> Result<(), String> {
|
|
|
40
40
|
Some("check") => check_cmd(&args.rest),
|
|
41
41
|
Some("doctor") => doctor_cmd(&args.rest),
|
|
42
42
|
Some("explain") => explain_cmd(&args.rest),
|
|
43
|
+
Some("explain-diff") => explain_diff_cmd(&args.rest),
|
|
43
44
|
Some("explain-model") | Some("walkthrough") => explain_model_cmd(&args.rest),
|
|
44
45
|
Some("demo") => demo_cmd(&args.rest),
|
|
45
46
|
Some("tour") => {
|
|
@@ -191,11 +192,15 @@ fn print_doctor_report(report: &DoctorReport) {
|
|
|
191
192
|
println!();
|
|
192
193
|
println!("Available commands:");
|
|
193
194
|
println!(
|
|
194
|
-
" inspect .cbm/.onnx/.json: {}",
|
|
195
|
+
" inspect .cbm/.lgb/.onnx/.json: {}",
|
|
195
196
|
available_unavailable(report.commands.inspect_artifacts)
|
|
196
197
|
);
|
|
197
198
|
println!(
|
|
198
|
-
" diff .cbm/.onnx: {}",
|
|
199
|
+
" diff .cbm/.lgb/.onnx: {}",
|
|
200
|
+
available_unavailable(report.commands.diff_artifacts)
|
|
201
|
+
);
|
|
202
|
+
println!(
|
|
203
|
+
" explain-diff .cbm/.lgb/.onnx: {}",
|
|
199
204
|
available_unavailable(report.commands.diff_artifacts)
|
|
200
205
|
);
|
|
201
206
|
println!(
|
|
@@ -389,6 +394,7 @@ fn diff_cmd(args: &[String]) -> Result<(), String> {
|
|
|
389
394
|
struct CheckOptions {
|
|
390
395
|
max_size_growth_pct: Option<f64>,
|
|
391
396
|
max_ops_growth_pct: Option<f64>,
|
|
397
|
+
max_parameter_growth_pct: Option<f64>,
|
|
392
398
|
fail_on_feature_change: bool,
|
|
393
399
|
fail_on_training_config_change: bool,
|
|
394
400
|
fail_on_new_op: bool,
|
|
@@ -415,6 +421,13 @@ fn check_cmd(args: &[String]) -> Result<(), String> {
|
|
|
415
421
|
options.max_ops_growth_pct = Some(parse_percent_threshold(value)?);
|
|
416
422
|
i += 2;
|
|
417
423
|
}
|
|
424
|
+
"--max-parameter-growth" => {
|
|
425
|
+
let value = args
|
|
426
|
+
.get(i + 1)
|
|
427
|
+
.ok_or_else(|| "--max-parameter-growth needs a percentage".to_string())?;
|
|
428
|
+
options.max_parameter_growth_pct = Some(parse_percent_threshold(value)?);
|
|
429
|
+
i += 2;
|
|
430
|
+
}
|
|
418
431
|
"--fail-on-feature-change" => {
|
|
419
432
|
options.fail_on_feature_change = true;
|
|
420
433
|
i += 1;
|
|
@@ -462,13 +475,16 @@ fn check_cmd(args: &[String]) -> Result<(), String> {
|
|
|
462
475
|
}
|
|
463
476
|
|
|
464
477
|
fn check_usage() -> String {
|
|
465
|
-
"usage: mod-trace check [--max-size-growth 20%] [--max-ops-growth 25%] [--fail-on-feature-change] [--fail-on-training-config-change] [--fail-on-new-op] <old-model> <new-model>".to_string()
|
|
478
|
+
"usage: mod-trace check [--max-size-growth 20%] [--max-ops-growth 25%] [--max-parameter-growth 30%] [--fail-on-feature-change] [--fail-on-training-config-change] [--fail-on-new-op] <old-model> <new-model>".to_string()
|
|
466
479
|
}
|
|
467
480
|
|
|
468
481
|
fn check_catboost(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<(), String> {
|
|
469
482
|
if options.max_ops_growth_pct.is_some() {
|
|
470
483
|
return Err("--max-ops-growth is only supported for ONNX artifacts.".to_string());
|
|
471
484
|
}
|
|
485
|
+
if options.max_parameter_growth_pct.is_some() {
|
|
486
|
+
return Err("--max-parameter-growth is only supported for ONNX artifacts.".to_string());
|
|
487
|
+
}
|
|
472
488
|
if options.fail_on_new_op {
|
|
473
489
|
return Err("--fail-on-new-op is only supported for ONNX artifacts.".to_string());
|
|
474
490
|
}
|
|
@@ -545,6 +561,15 @@ fn check_onnx(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<
|
|
|
545
561
|
));
|
|
546
562
|
}
|
|
547
563
|
|
|
564
|
+
if let Some(max_pct) = options.max_parameter_growth_pct {
|
|
565
|
+
checks.push(growth_check(
|
|
566
|
+
"parameter growth",
|
|
567
|
+
old.total_parameter_values,
|
|
568
|
+
new.total_parameter_values,
|
|
569
|
+
max_pct,
|
|
570
|
+
));
|
|
571
|
+
}
|
|
572
|
+
|
|
548
573
|
if options.fail_on_new_op {
|
|
549
574
|
let old_ops = old
|
|
550
575
|
.op_counts
|
|
@@ -972,6 +997,268 @@ fn explain_onnx_cmd(path: &str) -> Result<(), String> {
|
|
|
972
997
|
Ok(())
|
|
973
998
|
}
|
|
974
999
|
|
|
1000
|
+
fn explain_diff_cmd(args: &[String]) -> Result<(), String> {
|
|
1001
|
+
let paths = args
|
|
1002
|
+
.iter()
|
|
1003
|
+
.filter(|arg| !arg.starts_with("--"))
|
|
1004
|
+
.collect::<Vec<_>>();
|
|
1005
|
+
if paths.len() != 2 {
|
|
1006
|
+
return Err(
|
|
1007
|
+
"usage: mod-trace explain-diff <old-model> <new-model> (.onnx, .cbm, or .lgb/.txt)"
|
|
1008
|
+
.to_string(),
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
let (old, new) = (paths[0].as_str(), paths[1].as_str());
|
|
1012
|
+
match (artifact_kind(old), artifact_kind(new)) {
|
|
1013
|
+
(ArtifactKind::Onnx, ArtifactKind::Onnx) => explain_diff_onnx(old, new),
|
|
1014
|
+
(ArtifactKind::LightGbm, ArtifactKind::LightGbm) => explain_diff_lgbm(old, new),
|
|
1015
|
+
(ArtifactKind::CatBoost, ArtifactKind::CatBoost) => explain_diff_catboost(old, new),
|
|
1016
|
+
(left, right) => Err(format!(
|
|
1017
|
+
"explain-diff needs two artifacts of the same supported type (.onnx, .cbm, .lgb): {} vs {}",
|
|
1018
|
+
left.label(),
|
|
1019
|
+
right.label()
|
|
1020
|
+
)),
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
fn opt_num(value: Option<usize>) -> String {
|
|
1025
|
+
value
|
|
1026
|
+
.map(|value| value.to_string())
|
|
1027
|
+
.unwrap_or_else(|| "unknown".to_string())
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn format_count_human(count: usize) -> String {
|
|
1031
|
+
let value = count as f64;
|
|
1032
|
+
if value >= 1e9 {
|
|
1033
|
+
format!("{:.1}B", value / 1e9)
|
|
1034
|
+
} else if value >= 1e6 {
|
|
1035
|
+
format!("{:.1}M", value / 1e6)
|
|
1036
|
+
} else if value >= 1e3 {
|
|
1037
|
+
format!("{:.1}K", value / 1e3)
|
|
1038
|
+
} else {
|
|
1039
|
+
count.to_string()
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
fn growth_label(old: usize, new: usize) -> String {
|
|
1044
|
+
match growth_percent(old, new) {
|
|
1045
|
+
Some(pct) if pct.abs() < 0.005 => "same".to_string(),
|
|
1046
|
+
Some(pct) => format!("{pct:+.0}%"),
|
|
1047
|
+
None => {
|
|
1048
|
+
if new == old {
|
|
1049
|
+
"same".to_string()
|
|
1050
|
+
} else {
|
|
1051
|
+
"n/a (from zero)".to_string()
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
fn explain_diff_onnx(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1058
|
+
let old = onnx::inspect(old_path)?;
|
|
1059
|
+
let new = onnx::inspect(new_path)?;
|
|
1060
|
+
|
|
1061
|
+
let old_layers = estimate_transformer_layers(
|
|
1062
|
+
op_count(&old, "Softmax"),
|
|
1063
|
+
op_count(&old, "LayerNormalization"),
|
|
1064
|
+
op_count(&old, "Attention"),
|
|
1065
|
+
);
|
|
1066
|
+
let new_layers = estimate_transformer_layers(
|
|
1067
|
+
op_count(&new, "Softmax"),
|
|
1068
|
+
op_count(&new, "LayerNormalization"),
|
|
1069
|
+
op_count(&new, "Attention"),
|
|
1070
|
+
);
|
|
1071
|
+
let old_hidden = estimate_hidden_size(&old);
|
|
1072
|
+
let new_hidden = estimate_hidden_size(&new);
|
|
1073
|
+
|
|
1074
|
+
let old_ops = old
|
|
1075
|
+
.op_counts
|
|
1076
|
+
.iter()
|
|
1077
|
+
.map(|(name, _)| name)
|
|
1078
|
+
.collect::<BTreeSet<_>>();
|
|
1079
|
+
let new_ops = new
|
|
1080
|
+
.op_counts
|
|
1081
|
+
.iter()
|
|
1082
|
+
.map(|(name, _)| name)
|
|
1083
|
+
.collect::<BTreeSet<_>>();
|
|
1084
|
+
let added = new_ops
|
|
1085
|
+
.difference(&old_ops)
|
|
1086
|
+
.map(|name| (*name).clone())
|
|
1087
|
+
.collect::<Vec<_>>();
|
|
1088
|
+
let removed = old_ops
|
|
1089
|
+
.difference(&new_ops)
|
|
1090
|
+
.map(|name| (*name).clone())
|
|
1091
|
+
.collect::<Vec<_>>();
|
|
1092
|
+
|
|
1093
|
+
println!("Model Change Explanation");
|
|
1094
|
+
println!("------------------------");
|
|
1095
|
+
println!("Type: ONNX");
|
|
1096
|
+
println!("Old: {}", old.path);
|
|
1097
|
+
println!("New: {}", new.path);
|
|
1098
|
+
println!();
|
|
1099
|
+
println!("Architecture:");
|
|
1100
|
+
println!(
|
|
1101
|
+
" Attention layers: {} -> {}",
|
|
1102
|
+
opt_num(old_layers),
|
|
1103
|
+
opt_num(new_layers)
|
|
1104
|
+
);
|
|
1105
|
+
println!(
|
|
1106
|
+
" Hidden size: {} -> {}",
|
|
1107
|
+
opt_num(old_hidden),
|
|
1108
|
+
opt_num(new_hidden)
|
|
1109
|
+
);
|
|
1110
|
+
println!(
|
|
1111
|
+
" Parameters: {} -> {} ({})",
|
|
1112
|
+
format_count_human(old.total_parameter_values),
|
|
1113
|
+
format_count_human(new.total_parameter_values),
|
|
1114
|
+
growth_label(old.total_parameter_values, new.total_parameter_values)
|
|
1115
|
+
);
|
|
1116
|
+
println!(
|
|
1117
|
+
" Nodes: {} -> {} ({})",
|
|
1118
|
+
old.nodes.len(),
|
|
1119
|
+
new.nodes.len(),
|
|
1120
|
+
growth_label(old.nodes.len(), new.nodes.len())
|
|
1121
|
+
);
|
|
1122
|
+
println!();
|
|
1123
|
+
println!(
|
|
1124
|
+
"Estimated inference cost (static op proxy): {}",
|
|
1125
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1126
|
+
);
|
|
1127
|
+
println!();
|
|
1128
|
+
if added.is_empty() {
|
|
1129
|
+
println!("New operators introduced: none");
|
|
1130
|
+
} else {
|
|
1131
|
+
println!("New operators introduced:");
|
|
1132
|
+
for op in &added {
|
|
1133
|
+
println!(" {op}");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if !removed.is_empty() {
|
|
1137
|
+
println!("Operators removed:");
|
|
1138
|
+
for op in &removed {
|
|
1139
|
+
println!(" {op}");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
println!();
|
|
1143
|
+
println!("Summary:");
|
|
1144
|
+
match (old_layers, new_layers) {
|
|
1145
|
+
(Some(a), Some(b)) if a != b => println!(
|
|
1146
|
+
" Grew from ~{a} to ~{b} attention layers; parameters {}, estimated cost {}.",
|
|
1147
|
+
growth_label(old.total_parameter_values, new.total_parameter_values),
|
|
1148
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1149
|
+
),
|
|
1150
|
+
_ => println!(
|
|
1151
|
+
" Parameters {}, estimated cost {}.",
|
|
1152
|
+
growth_label(old.total_parameter_values, new.total_parameter_values),
|
|
1153
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1154
|
+
),
|
|
1155
|
+
}
|
|
1156
|
+
println!();
|
|
1157
|
+
println!(
|
|
1158
|
+
"Note: static heuristic over ONNX ops and shapes; cost is an op-count proxy, not measured latency."
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
Ok(())
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
fn explain_diff_lgbm(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1165
|
+
let old = lgbm::inspect(old_path)?;
|
|
1166
|
+
let new = lgbm::inspect(new_path)?;
|
|
1167
|
+
|
|
1168
|
+
println!("Model Change Explanation");
|
|
1169
|
+
println!("------------------------");
|
|
1170
|
+
println!("Type: LightGBM");
|
|
1171
|
+
println!("Old: {}", old.path);
|
|
1172
|
+
println!("New: {}", new.path);
|
|
1173
|
+
println!();
|
|
1174
|
+
println!("Architecture:");
|
|
1175
|
+
println!(
|
|
1176
|
+
" Trees: {} -> {} ({})",
|
|
1177
|
+
old.num_trees,
|
|
1178
|
+
new.num_trees,
|
|
1179
|
+
growth_label(old.num_trees as usize, new.num_trees as usize)
|
|
1180
|
+
);
|
|
1181
|
+
println!(
|
|
1182
|
+
" Leaves / tree: {} -> {}",
|
|
1183
|
+
opt_num(old.num_leaves.map(|value| value as usize)),
|
|
1184
|
+
opt_num(new.num_leaves.map(|value| value as usize))
|
|
1185
|
+
);
|
|
1186
|
+
println!(
|
|
1187
|
+
" Features: {} -> {}",
|
|
1188
|
+
opt_num(old.num_features.map(|value| value as usize)),
|
|
1189
|
+
opt_num(new.num_features.map(|value| value as usize))
|
|
1190
|
+
);
|
|
1191
|
+
println!(
|
|
1192
|
+
" Objective: {} -> {}",
|
|
1193
|
+
old.objective.as_deref().unwrap_or("unknown"),
|
|
1194
|
+
new.objective.as_deref().unwrap_or("unknown")
|
|
1195
|
+
);
|
|
1196
|
+
println!();
|
|
1197
|
+
match (old.estimated_leaf_values(), new.estimated_leaf_values()) {
|
|
1198
|
+
(Some(o), Some(n)) => println!(
|
|
1199
|
+
"Estimated leaf-slot growth: {}",
|
|
1200
|
+
growth_label(o as usize, n as usize)
|
|
1201
|
+
),
|
|
1202
|
+
_ => println!("Estimated leaf-slot growth: unknown"),
|
|
1203
|
+
}
|
|
1204
|
+
println!();
|
|
1205
|
+
if old.learned_state_fingerprint != new.learned_state_fingerprint {
|
|
1206
|
+
println!("Learned state: CHANGED (a real retrain - leaf values differ)");
|
|
1207
|
+
} else {
|
|
1208
|
+
println!("Learned state: unchanged (identical leaf values)");
|
|
1209
|
+
}
|
|
1210
|
+
println!();
|
|
1211
|
+
println!("Note: parsed natively from the LightGBM text model.");
|
|
1212
|
+
|
|
1213
|
+
Ok(())
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
fn explain_diff_catboost(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1217
|
+
let old = cbm::inspect(old_path)?;
|
|
1218
|
+
let new = cbm::inspect(new_path)?;
|
|
1219
|
+
|
|
1220
|
+
println!("Model Change Explanation");
|
|
1221
|
+
println!("------------------------");
|
|
1222
|
+
println!("Type: CatBoost");
|
|
1223
|
+
println!("Old: {}", old.path);
|
|
1224
|
+
println!("New: {}", new.path);
|
|
1225
|
+
println!();
|
|
1226
|
+
println!("Architecture:");
|
|
1227
|
+
println!(
|
|
1228
|
+
" Trees: {} -> {}",
|
|
1229
|
+
opt_num(old.iterations.map(|value| value as usize)),
|
|
1230
|
+
opt_num(new.iterations.map(|value| value as usize))
|
|
1231
|
+
);
|
|
1232
|
+
println!(
|
|
1233
|
+
" Depth: {} -> {}",
|
|
1234
|
+
opt_num(old.depth.map(|value| value as usize)),
|
|
1235
|
+
opt_num(new.depth.map(|value| value as usize))
|
|
1236
|
+
);
|
|
1237
|
+
println!(
|
|
1238
|
+
" Features (recovered): {} -> {}",
|
|
1239
|
+
old.feature_candidates.len(),
|
|
1240
|
+
new.feature_candidates.len()
|
|
1241
|
+
);
|
|
1242
|
+
println!();
|
|
1243
|
+
match (old.estimated_leaf_values(), new.estimated_leaf_values()) {
|
|
1244
|
+
(Some(o), Some(n)) => println!(
|
|
1245
|
+
"Estimated leaf-slot growth: {}",
|
|
1246
|
+
growth_label(o as usize, n as usize)
|
|
1247
|
+
),
|
|
1248
|
+
_ => println!("Estimated leaf-slot growth: unknown"),
|
|
1249
|
+
}
|
|
1250
|
+
println!();
|
|
1251
|
+
if old.learned_state_fingerprint != new.learned_state_fingerprint {
|
|
1252
|
+
println!("Learned state: CHANGED (split borders / leaf values differ)");
|
|
1253
|
+
} else {
|
|
1254
|
+
println!("Learned state: unchanged");
|
|
1255
|
+
}
|
|
1256
|
+
println!();
|
|
1257
|
+
println!("Note: CatBoost internals are summarized; run `diff --deep` for exact split/leaf changes.");
|
|
1258
|
+
|
|
1259
|
+
Ok(())
|
|
1260
|
+
}
|
|
1261
|
+
|
|
975
1262
|
fn op_count(report: &onnx::OnnxReport, op_type: &str) -> usize {
|
|
976
1263
|
report
|
|
977
1264
|
.op_counts
|
|
@@ -2549,6 +2836,9 @@ fn check_lgbm(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<
|
|
|
2549
2836
|
if options.max_ops_growth_pct.is_some() {
|
|
2550
2837
|
return Err("--max-ops-growth is only supported for ONNX artifacts.".to_string());
|
|
2551
2838
|
}
|
|
2839
|
+
if options.max_parameter_growth_pct.is_some() {
|
|
2840
|
+
return Err("--max-parameter-growth is only supported for ONNX artifacts.".to_string());
|
|
2841
|
+
}
|
|
2552
2842
|
if options.fail_on_new_op {
|
|
2553
2843
|
return Err("--fail-on-new-op is only supported for ONNX artifacts.".to_string());
|
|
2554
2844
|
}
|
|
@@ -3090,19 +3380,16 @@ fn print_help() {
|
|
|
3090
3380
|
Core usage:\n \
|
|
3091
3381
|
mod-trace doctor [--json]\n \
|
|
3092
3382
|
mod-trace inspect [--deep] [--json] [--limit 20] <model.cbm|model.lgb|model.onnx|model.json>\n \
|
|
3093
|
-
mod-trace diff [--deep] [--json] <old-model> <new-model> (.cbm, .lgb/.txt LightGBM, or .onnx)\n\
|
|
3094
|
-
mod-trace
|
|
3383
|
+
mod-trace diff [--deep] [--json] <old-model> <new-model> (.cbm, .lgb/.txt LightGBM, or .onnx)\n \
|
|
3384
|
+
mod-trace explain-diff <old-model> <new-model> (plain-English what changed: layers, params, cost, new ops)\n\n\
|
|
3385
|
+
mod-trace check [--max-size-growth 20%] [--max-ops-growth 25%] [--max-parameter-growth 30%] [--fail-on-feature-change] [--fail-on-training-config-change] [--fail-on-new-op] <old-model> <new-model>\n\n\
|
|
3095
3386
|
Artifact inspectors:\n \
|
|
3096
3387
|
mod-trace catboost [--deep] [--json] [--limit 20] <model.cbm> [more.cbm...]\n \
|
|
3097
3388
|
mod-trace lightgbm [--json] [--limit 20] <model.lgb|model.txt> [more...]\n \
|
|
3098
|
-
mod-trace onnx [--json] [--limit 20] <model.onnx>\n\
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
mod-trace compare <model.json>\n \
|
|
3102
|
-
mod-trace why <model.json>\n \
|
|
3103
|
-
mod-trace validate <model.json>\n \
|
|
3104
|
-
mod-trace tensor-inspect <model.json>\n \
|
|
3105
|
-
mod-trace explain <model.onnx|model.cbm>\n \
|
|
3389
|
+
mod-trace onnx [--json] [--limit 20] <model.onnx>\n \
|
|
3390
|
+
mod-trace explain <model.onnx|model.cbm>\n\n\
|
|
3391
|
+
Tensor lab (secondary; see docs/tensor-lab.md):\n \
|
|
3392
|
+
mod-trace trace|compare|why|validate|tensor-inspect <model.json>\n \
|
|
3106
3393
|
mod-trace explain <topic> [--step] [--shapes|--memory|--math|--compare] [--quiz]\n\n\
|
|
3107
3394
|
Examples:\n \
|
|
3108
3395
|
mod-trace doctor\n \
|
|
@@ -3205,6 +3492,22 @@ fn format_hex(value: u64) -> String {
|
|
|
3205
3492
|
mod tests {
|
|
3206
3493
|
use super::*;
|
|
3207
3494
|
|
|
3495
|
+
#[test]
|
|
3496
|
+
fn format_count_human_scales_units() {
|
|
3497
|
+
assert_eq!(format_count_human(676), "676");
|
|
3498
|
+
assert_eq!(format_count_human(19_204), "19.2K");
|
|
3499
|
+
assert_eq!(format_count_human(110_000_000), "110.0M");
|
|
3500
|
+
assert_eq!(format_count_human(2_200_000_000), "2.2B");
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
#[test]
|
|
3504
|
+
fn growth_label_formats_changes() {
|
|
3505
|
+
assert_eq!(growth_label(100, 200), "+100%");
|
|
3506
|
+
assert_eq!(growth_label(200, 100), "-50%");
|
|
3507
|
+
assert_eq!(growth_label(100, 100), "same");
|
|
3508
|
+
assert_eq!(growth_label(0, 5), "n/a (from zero)");
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3208
3511
|
#[test]
|
|
3209
3512
|
fn estimates_transformer_layers_from_attention_signals() {
|
|
3210
3513
|
assert_eq!(estimate_transformer_layers(12, 24, 0), Some(12));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"sessionId":"a6f7a6f9-0897-4154-80fe-a91a5084903f","pid":34928,"procStart":"Thu Jun 4 15:37:09 2026","acquiredAt":1780606199550}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import numpy as np, onnx, os
|
|
2
|
-
from onnx import helper, TensorProto, numpy_helper
|
|
3
|
-
|
|
4
|
-
def build(path, seed, producer_ver, hidden=64, classes=4):
|
|
5
|
-
rng = np.random.default_rng(seed)
|
|
6
|
-
def arr(name, *shape):
|
|
7
|
-
return numpy_helper.from_array(rng.standard_normal(shape).astype(np.float32) * 0.1, name)
|
|
8
|
-
inp = helper.make_tensor_value_info("input", TensorProto.FLOAT, [1, 16])
|
|
9
|
-
out = helper.make_tensor_value_info("logits", TensorProto.FLOAT, [1, classes])
|
|
10
|
-
inits = [arr("W1", 16, hidden), arr("b1", hidden), arr("W2", hidden, classes), arr("b2", classes)]
|
|
11
|
-
nodes = [
|
|
12
|
-
helper.make_node("Gemm", ["input", "W1", "b1"], ["h1"]),
|
|
13
|
-
helper.make_node("Relu", ["h1"], ["a1"]),
|
|
14
|
-
helper.make_node("Gemm", ["a1", "W2", "b2"], ["z"]),
|
|
15
|
-
helper.make_node("Identity", ["z"], ["logits"]),
|
|
16
|
-
]
|
|
17
|
-
g = helper.make_graph(nodes, "classifier", [inp], [out], initializer=inits)
|
|
18
|
-
m = helper.make_model(g, producer_name="demo-trainer", producer_version=producer_ver,
|
|
19
|
-
opset_imports=[helper.make_opsetid("", 13)])
|
|
20
|
-
onnx.checker.check_model(m)
|
|
21
|
-
onnx.save(m, path)
|
|
22
|
-
print("wrote", path)
|
|
23
|
-
|
|
24
|
-
os.makedirs("/tmp/mt-demo", exist_ok=True)
|
|
25
|
-
# same architecture & shapes; only the learned weights (and producer version) differ -> a "retrain"
|
|
26
|
-
build("/tmp/mt-demo/model_a.onnx", seed=1, producer_ver="1.0")
|
|
27
|
-
build("/tmp/mt-demo/model_b.onnx", seed=2, producer_ver="1.1")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|