mod-trace 0.2.0__tar.gz → 0.3.0__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.0}/.gitignore +2 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/Cargo.lock +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.0}/Cargo.toml +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.0}/PKG-INFO +77 -57
- {mod_trace-0.2.0 → mod_trace-0.3.0}/README.md +76 -56
- mod_trace-0.3.0/docs/tensor-lab.md +66 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/pyproject.toml +1 -1
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/main.rs +310 -11
- 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.0}/.github/workflows/release.yml +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/LICENSE +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/benchmarks/tiny_pytorch.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/docs/ARCHITECTURE.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/docs/REAL_MODELS.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/broken_shape.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/lightgbm/README.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/lightgbm/clf_v1.txt +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/lightgbm/clf_v2.txt +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/lightgbm/generate_demo_models.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/make_sample_catboost.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/mlp.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/README.md +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/generate_demo_models.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/mlp_retrain_a.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/mlp_retrain_b.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/mlp_v1.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/onnx/mlp_v2.onnx +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/tiny_attention.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/examples/tiny_attention_plan.json +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/catboost_deep_diff.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/catboost_explain.py +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/cbm.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/demo.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/explain.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/lgbm.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/model.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/src/onnx.rs +0 -0
- {mod_trace-0.2.0 → mod_trace-0.3.0}/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.0
|
|
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") => {
|
|
@@ -389,6 +390,7 @@ fn diff_cmd(args: &[String]) -> Result<(), String> {
|
|
|
389
390
|
struct CheckOptions {
|
|
390
391
|
max_size_growth_pct: Option<f64>,
|
|
391
392
|
max_ops_growth_pct: Option<f64>,
|
|
393
|
+
max_parameter_growth_pct: Option<f64>,
|
|
392
394
|
fail_on_feature_change: bool,
|
|
393
395
|
fail_on_training_config_change: bool,
|
|
394
396
|
fail_on_new_op: bool,
|
|
@@ -415,6 +417,13 @@ fn check_cmd(args: &[String]) -> Result<(), String> {
|
|
|
415
417
|
options.max_ops_growth_pct = Some(parse_percent_threshold(value)?);
|
|
416
418
|
i += 2;
|
|
417
419
|
}
|
|
420
|
+
"--max-parameter-growth" => {
|
|
421
|
+
let value = args
|
|
422
|
+
.get(i + 1)
|
|
423
|
+
.ok_or_else(|| "--max-parameter-growth needs a percentage".to_string())?;
|
|
424
|
+
options.max_parameter_growth_pct = Some(parse_percent_threshold(value)?);
|
|
425
|
+
i += 2;
|
|
426
|
+
}
|
|
418
427
|
"--fail-on-feature-change" => {
|
|
419
428
|
options.fail_on_feature_change = true;
|
|
420
429
|
i += 1;
|
|
@@ -462,13 +471,16 @@ fn check_cmd(args: &[String]) -> Result<(), String> {
|
|
|
462
471
|
}
|
|
463
472
|
|
|
464
473
|
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()
|
|
474
|
+
"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
475
|
}
|
|
467
476
|
|
|
468
477
|
fn check_catboost(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<(), String> {
|
|
469
478
|
if options.max_ops_growth_pct.is_some() {
|
|
470
479
|
return Err("--max-ops-growth is only supported for ONNX artifacts.".to_string());
|
|
471
480
|
}
|
|
481
|
+
if options.max_parameter_growth_pct.is_some() {
|
|
482
|
+
return Err("--max-parameter-growth is only supported for ONNX artifacts.".to_string());
|
|
483
|
+
}
|
|
472
484
|
if options.fail_on_new_op {
|
|
473
485
|
return Err("--fail-on-new-op is only supported for ONNX artifacts.".to_string());
|
|
474
486
|
}
|
|
@@ -545,6 +557,15 @@ fn check_onnx(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<
|
|
|
545
557
|
));
|
|
546
558
|
}
|
|
547
559
|
|
|
560
|
+
if let Some(max_pct) = options.max_parameter_growth_pct {
|
|
561
|
+
checks.push(growth_check(
|
|
562
|
+
"parameter growth",
|
|
563
|
+
old.total_parameter_values,
|
|
564
|
+
new.total_parameter_values,
|
|
565
|
+
max_pct,
|
|
566
|
+
));
|
|
567
|
+
}
|
|
568
|
+
|
|
548
569
|
if options.fail_on_new_op {
|
|
549
570
|
let old_ops = old
|
|
550
571
|
.op_counts
|
|
@@ -972,6 +993,268 @@ fn explain_onnx_cmd(path: &str) -> Result<(), String> {
|
|
|
972
993
|
Ok(())
|
|
973
994
|
}
|
|
974
995
|
|
|
996
|
+
fn explain_diff_cmd(args: &[String]) -> Result<(), String> {
|
|
997
|
+
let paths = args
|
|
998
|
+
.iter()
|
|
999
|
+
.filter(|arg| !arg.starts_with("--"))
|
|
1000
|
+
.collect::<Vec<_>>();
|
|
1001
|
+
if paths.len() != 2 {
|
|
1002
|
+
return Err(
|
|
1003
|
+
"usage: mod-trace explain-diff <old-model> <new-model> (.onnx, .cbm, or .lgb/.txt)"
|
|
1004
|
+
.to_string(),
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
let (old, new) = (paths[0].as_str(), paths[1].as_str());
|
|
1008
|
+
match (artifact_kind(old), artifact_kind(new)) {
|
|
1009
|
+
(ArtifactKind::Onnx, ArtifactKind::Onnx) => explain_diff_onnx(old, new),
|
|
1010
|
+
(ArtifactKind::LightGbm, ArtifactKind::LightGbm) => explain_diff_lgbm(old, new),
|
|
1011
|
+
(ArtifactKind::CatBoost, ArtifactKind::CatBoost) => explain_diff_catboost(old, new),
|
|
1012
|
+
(left, right) => Err(format!(
|
|
1013
|
+
"explain-diff needs two artifacts of the same supported type (.onnx, .cbm, .lgb): {} vs {}",
|
|
1014
|
+
left.label(),
|
|
1015
|
+
right.label()
|
|
1016
|
+
)),
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
fn opt_num(value: Option<usize>) -> String {
|
|
1021
|
+
value
|
|
1022
|
+
.map(|value| value.to_string())
|
|
1023
|
+
.unwrap_or_else(|| "unknown".to_string())
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
fn format_count_human(count: usize) -> String {
|
|
1027
|
+
let value = count as f64;
|
|
1028
|
+
if value >= 1e9 {
|
|
1029
|
+
format!("{:.1}B", value / 1e9)
|
|
1030
|
+
} else if value >= 1e6 {
|
|
1031
|
+
format!("{:.1}M", value / 1e6)
|
|
1032
|
+
} else if value >= 1e3 {
|
|
1033
|
+
format!("{:.1}K", value / 1e3)
|
|
1034
|
+
} else {
|
|
1035
|
+
count.to_string()
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
fn growth_label(old: usize, new: usize) -> String {
|
|
1040
|
+
match growth_percent(old, new) {
|
|
1041
|
+
Some(pct) if pct.abs() < 0.005 => "same".to_string(),
|
|
1042
|
+
Some(pct) => format!("{pct:+.0}%"),
|
|
1043
|
+
None => {
|
|
1044
|
+
if new == old {
|
|
1045
|
+
"same".to_string()
|
|
1046
|
+
} else {
|
|
1047
|
+
"n/a (from zero)".to_string()
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
fn explain_diff_onnx(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1054
|
+
let old = onnx::inspect(old_path)?;
|
|
1055
|
+
let new = onnx::inspect(new_path)?;
|
|
1056
|
+
|
|
1057
|
+
let old_layers = estimate_transformer_layers(
|
|
1058
|
+
op_count(&old, "Softmax"),
|
|
1059
|
+
op_count(&old, "LayerNormalization"),
|
|
1060
|
+
op_count(&old, "Attention"),
|
|
1061
|
+
);
|
|
1062
|
+
let new_layers = estimate_transformer_layers(
|
|
1063
|
+
op_count(&new, "Softmax"),
|
|
1064
|
+
op_count(&new, "LayerNormalization"),
|
|
1065
|
+
op_count(&new, "Attention"),
|
|
1066
|
+
);
|
|
1067
|
+
let old_hidden = estimate_hidden_size(&old);
|
|
1068
|
+
let new_hidden = estimate_hidden_size(&new);
|
|
1069
|
+
|
|
1070
|
+
let old_ops = old
|
|
1071
|
+
.op_counts
|
|
1072
|
+
.iter()
|
|
1073
|
+
.map(|(name, _)| name)
|
|
1074
|
+
.collect::<BTreeSet<_>>();
|
|
1075
|
+
let new_ops = new
|
|
1076
|
+
.op_counts
|
|
1077
|
+
.iter()
|
|
1078
|
+
.map(|(name, _)| name)
|
|
1079
|
+
.collect::<BTreeSet<_>>();
|
|
1080
|
+
let added = new_ops
|
|
1081
|
+
.difference(&old_ops)
|
|
1082
|
+
.map(|name| (*name).clone())
|
|
1083
|
+
.collect::<Vec<_>>();
|
|
1084
|
+
let removed = old_ops
|
|
1085
|
+
.difference(&new_ops)
|
|
1086
|
+
.map(|name| (*name).clone())
|
|
1087
|
+
.collect::<Vec<_>>();
|
|
1088
|
+
|
|
1089
|
+
println!("Model Change Explanation");
|
|
1090
|
+
println!("------------------------");
|
|
1091
|
+
println!("Type: ONNX");
|
|
1092
|
+
println!("Old: {}", old.path);
|
|
1093
|
+
println!("New: {}", new.path);
|
|
1094
|
+
println!();
|
|
1095
|
+
println!("Architecture:");
|
|
1096
|
+
println!(
|
|
1097
|
+
" Attention layers: {} -> {}",
|
|
1098
|
+
opt_num(old_layers),
|
|
1099
|
+
opt_num(new_layers)
|
|
1100
|
+
);
|
|
1101
|
+
println!(
|
|
1102
|
+
" Hidden size: {} -> {}",
|
|
1103
|
+
opt_num(old_hidden),
|
|
1104
|
+
opt_num(new_hidden)
|
|
1105
|
+
);
|
|
1106
|
+
println!(
|
|
1107
|
+
" Parameters: {} -> {} ({})",
|
|
1108
|
+
format_count_human(old.total_parameter_values),
|
|
1109
|
+
format_count_human(new.total_parameter_values),
|
|
1110
|
+
growth_label(old.total_parameter_values, new.total_parameter_values)
|
|
1111
|
+
);
|
|
1112
|
+
println!(
|
|
1113
|
+
" Nodes: {} -> {} ({})",
|
|
1114
|
+
old.nodes.len(),
|
|
1115
|
+
new.nodes.len(),
|
|
1116
|
+
growth_label(old.nodes.len(), new.nodes.len())
|
|
1117
|
+
);
|
|
1118
|
+
println!();
|
|
1119
|
+
println!(
|
|
1120
|
+
"Estimated inference cost (static op proxy): {}",
|
|
1121
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1122
|
+
);
|
|
1123
|
+
println!();
|
|
1124
|
+
if added.is_empty() {
|
|
1125
|
+
println!("New operators introduced: none");
|
|
1126
|
+
} else {
|
|
1127
|
+
println!("New operators introduced:");
|
|
1128
|
+
for op in &added {
|
|
1129
|
+
println!(" {op}");
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if !removed.is_empty() {
|
|
1133
|
+
println!("Operators removed:");
|
|
1134
|
+
for op in &removed {
|
|
1135
|
+
println!(" {op}");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
println!();
|
|
1139
|
+
println!("Summary:");
|
|
1140
|
+
match (old_layers, new_layers) {
|
|
1141
|
+
(Some(a), Some(b)) if a != b => println!(
|
|
1142
|
+
" Grew from ~{a} to ~{b} attention layers; parameters {}, estimated cost {}.",
|
|
1143
|
+
growth_label(old.total_parameter_values, new.total_parameter_values),
|
|
1144
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1145
|
+
),
|
|
1146
|
+
_ => println!(
|
|
1147
|
+
" Parameters {}, estimated cost {}.",
|
|
1148
|
+
growth_label(old.total_parameter_values, new.total_parameter_values),
|
|
1149
|
+
growth_label(old.estimated_ops, new.estimated_ops)
|
|
1150
|
+
),
|
|
1151
|
+
}
|
|
1152
|
+
println!();
|
|
1153
|
+
println!(
|
|
1154
|
+
"Note: static heuristic over ONNX ops and shapes; cost is an op-count proxy, not measured latency."
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
Ok(())
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn explain_diff_lgbm(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1161
|
+
let old = lgbm::inspect(old_path)?;
|
|
1162
|
+
let new = lgbm::inspect(new_path)?;
|
|
1163
|
+
|
|
1164
|
+
println!("Model Change Explanation");
|
|
1165
|
+
println!("------------------------");
|
|
1166
|
+
println!("Type: LightGBM");
|
|
1167
|
+
println!("Old: {}", old.path);
|
|
1168
|
+
println!("New: {}", new.path);
|
|
1169
|
+
println!();
|
|
1170
|
+
println!("Architecture:");
|
|
1171
|
+
println!(
|
|
1172
|
+
" Trees: {} -> {} ({})",
|
|
1173
|
+
old.num_trees,
|
|
1174
|
+
new.num_trees,
|
|
1175
|
+
growth_label(old.num_trees as usize, new.num_trees as usize)
|
|
1176
|
+
);
|
|
1177
|
+
println!(
|
|
1178
|
+
" Leaves / tree: {} -> {}",
|
|
1179
|
+
opt_num(old.num_leaves.map(|value| value as usize)),
|
|
1180
|
+
opt_num(new.num_leaves.map(|value| value as usize))
|
|
1181
|
+
);
|
|
1182
|
+
println!(
|
|
1183
|
+
" Features: {} -> {}",
|
|
1184
|
+
opt_num(old.num_features.map(|value| value as usize)),
|
|
1185
|
+
opt_num(new.num_features.map(|value| value as usize))
|
|
1186
|
+
);
|
|
1187
|
+
println!(
|
|
1188
|
+
" Objective: {} -> {}",
|
|
1189
|
+
old.objective.as_deref().unwrap_or("unknown"),
|
|
1190
|
+
new.objective.as_deref().unwrap_or("unknown")
|
|
1191
|
+
);
|
|
1192
|
+
println!();
|
|
1193
|
+
match (old.estimated_leaf_values(), new.estimated_leaf_values()) {
|
|
1194
|
+
(Some(o), Some(n)) => println!(
|
|
1195
|
+
"Estimated leaf-slot growth: {}",
|
|
1196
|
+
growth_label(o as usize, n as usize)
|
|
1197
|
+
),
|
|
1198
|
+
_ => println!("Estimated leaf-slot growth: unknown"),
|
|
1199
|
+
}
|
|
1200
|
+
println!();
|
|
1201
|
+
if old.learned_state_fingerprint != new.learned_state_fingerprint {
|
|
1202
|
+
println!("Learned state: CHANGED (a real retrain - leaf values differ)");
|
|
1203
|
+
} else {
|
|
1204
|
+
println!("Learned state: unchanged (identical leaf values)");
|
|
1205
|
+
}
|
|
1206
|
+
println!();
|
|
1207
|
+
println!("Note: parsed natively from the LightGBM text model.");
|
|
1208
|
+
|
|
1209
|
+
Ok(())
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
fn explain_diff_catboost(old_path: &str, new_path: &str) -> Result<(), String> {
|
|
1213
|
+
let old = cbm::inspect(old_path)?;
|
|
1214
|
+
let new = cbm::inspect(new_path)?;
|
|
1215
|
+
|
|
1216
|
+
println!("Model Change Explanation");
|
|
1217
|
+
println!("------------------------");
|
|
1218
|
+
println!("Type: CatBoost");
|
|
1219
|
+
println!("Old: {}", old.path);
|
|
1220
|
+
println!("New: {}", new.path);
|
|
1221
|
+
println!();
|
|
1222
|
+
println!("Architecture:");
|
|
1223
|
+
println!(
|
|
1224
|
+
" Trees: {} -> {}",
|
|
1225
|
+
opt_num(old.iterations.map(|value| value as usize)),
|
|
1226
|
+
opt_num(new.iterations.map(|value| value as usize))
|
|
1227
|
+
);
|
|
1228
|
+
println!(
|
|
1229
|
+
" Depth: {} -> {}",
|
|
1230
|
+
opt_num(old.depth.map(|value| value as usize)),
|
|
1231
|
+
opt_num(new.depth.map(|value| value as usize))
|
|
1232
|
+
);
|
|
1233
|
+
println!(
|
|
1234
|
+
" Features (recovered): {} -> {}",
|
|
1235
|
+
old.feature_candidates.len(),
|
|
1236
|
+
new.feature_candidates.len()
|
|
1237
|
+
);
|
|
1238
|
+
println!();
|
|
1239
|
+
match (old.estimated_leaf_values(), new.estimated_leaf_values()) {
|
|
1240
|
+
(Some(o), Some(n)) => println!(
|
|
1241
|
+
"Estimated leaf-slot growth: {}",
|
|
1242
|
+
growth_label(o as usize, n as usize)
|
|
1243
|
+
),
|
|
1244
|
+
_ => println!("Estimated leaf-slot growth: unknown"),
|
|
1245
|
+
}
|
|
1246
|
+
println!();
|
|
1247
|
+
if old.learned_state_fingerprint != new.learned_state_fingerprint {
|
|
1248
|
+
println!("Learned state: CHANGED (split borders / leaf values differ)");
|
|
1249
|
+
} else {
|
|
1250
|
+
println!("Learned state: unchanged");
|
|
1251
|
+
}
|
|
1252
|
+
println!();
|
|
1253
|
+
println!("Note: CatBoost internals are summarized; run `diff --deep` for exact split/leaf changes.");
|
|
1254
|
+
|
|
1255
|
+
Ok(())
|
|
1256
|
+
}
|
|
1257
|
+
|
|
975
1258
|
fn op_count(report: &onnx::OnnxReport, op_type: &str) -> usize {
|
|
976
1259
|
report
|
|
977
1260
|
.op_counts
|
|
@@ -2549,6 +2832,9 @@ fn check_lgbm(old_path: &str, new_path: &str, options: &CheckOptions) -> Result<
|
|
|
2549
2832
|
if options.max_ops_growth_pct.is_some() {
|
|
2550
2833
|
return Err("--max-ops-growth is only supported for ONNX artifacts.".to_string());
|
|
2551
2834
|
}
|
|
2835
|
+
if options.max_parameter_growth_pct.is_some() {
|
|
2836
|
+
return Err("--max-parameter-growth is only supported for ONNX artifacts.".to_string());
|
|
2837
|
+
}
|
|
2552
2838
|
if options.fail_on_new_op {
|
|
2553
2839
|
return Err("--fail-on-new-op is only supported for ONNX artifacts.".to_string());
|
|
2554
2840
|
}
|
|
@@ -3090,19 +3376,16 @@ fn print_help() {
|
|
|
3090
3376
|
Core usage:\n \
|
|
3091
3377
|
mod-trace doctor [--json]\n \
|
|
3092
3378
|
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
|
|
3379
|
+
mod-trace diff [--deep] [--json] <old-model> <new-model> (.cbm, .lgb/.txt LightGBM, or .onnx)\n \
|
|
3380
|
+
mod-trace explain-diff <old-model> <new-model> (plain-English what changed: layers, params, cost, new ops)\n\n\
|
|
3381
|
+
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
3382
|
Artifact inspectors:\n \
|
|
3096
3383
|
mod-trace catboost [--deep] [--json] [--limit 20] <model.cbm> [more.cbm...]\n \
|
|
3097
3384
|
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 \
|
|
3385
|
+
mod-trace onnx [--json] [--limit 20] <model.onnx>\n \
|
|
3386
|
+
mod-trace explain <model.onnx|model.cbm>\n\n\
|
|
3387
|
+
Tensor lab (secondary; see docs/tensor-lab.md):\n \
|
|
3388
|
+
mod-trace trace|compare|why|validate|tensor-inspect <model.json>\n \
|
|
3106
3389
|
mod-trace explain <topic> [--step] [--shapes|--memory|--math|--compare] [--quiz]\n\n\
|
|
3107
3390
|
Examples:\n \
|
|
3108
3391
|
mod-trace doctor\n \
|
|
@@ -3205,6 +3488,22 @@ fn format_hex(value: u64) -> String {
|
|
|
3205
3488
|
mod tests {
|
|
3206
3489
|
use super::*;
|
|
3207
3490
|
|
|
3491
|
+
#[test]
|
|
3492
|
+
fn format_count_human_scales_units() {
|
|
3493
|
+
assert_eq!(format_count_human(676), "676");
|
|
3494
|
+
assert_eq!(format_count_human(19_204), "19.2K");
|
|
3495
|
+
assert_eq!(format_count_human(110_000_000), "110.0M");
|
|
3496
|
+
assert_eq!(format_count_human(2_200_000_000), "2.2B");
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
#[test]
|
|
3500
|
+
fn growth_label_formats_changes() {
|
|
3501
|
+
assert_eq!(growth_label(100, 200), "+100%");
|
|
3502
|
+
assert_eq!(growth_label(200, 100), "-50%");
|
|
3503
|
+
assert_eq!(growth_label(100, 100), "same");
|
|
3504
|
+
assert_eq!(growth_label(0, 5), "n/a (from zero)");
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3208
3507
|
#[test]
|
|
3209
3508
|
fn estimates_transformer_layers_from_attention_signals() {
|
|
3210
3509
|
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
|