metrik-explorer 0.1.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.
- metrik_explorer-0.1.0/.gitignore +89 -0
- metrik_explorer-0.1.0/PKG-INFO +56 -0
- metrik_explorer-0.1.0/README.md +40 -0
- metrik_explorer-0.1.0/pyproject.toml +44 -0
- metrik_explorer-0.1.0/src/metrik/explorer/__init__.py +44 -0
- metrik_explorer-0.1.0/src/metrik/explorer/metrik_plugin.json +20 -0
- metrik_explorer-0.1.0/src/metrik/explorer/model_dir.py +132 -0
- metrik_explorer-0.1.0/src/metrik/explorer/producer.py +779 -0
- metrik_explorer-0.1.0/src/metrik/explorer/py.typed +0 -0
- metrik_explorer-0.1.0/src/metrik/explorer/roles.py +148 -0
- metrik_explorer-0.1.0/src/metrik/explorer/run.py +105 -0
- metrik_explorer-0.1.0/src/metrik/explorer/safetensors.py +185 -0
- metrik_explorer-0.1.0/src/metrik/explorer/topology.py +150 -0
- metrik_explorer-0.1.0/tests/conftest.py +124 -0
- metrik_explorer-0.1.0/tests/test_explore.py +484 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Metrik — .gitignore
|
|
2
|
+
#
|
|
3
|
+
# NOTE: the previous version of this file contained `docs/*`, `.gitignore`, and `.claude`,
|
|
4
|
+
# which excluded the entire documentation tree — i.e. every Phase 0/1 deliverable — from
|
|
5
|
+
# version control. Replaced deliberately. docs/ is the project's primary output right now.
|
|
6
|
+
|
|
7
|
+
# --- Build output that must never be committed ---
|
|
8
|
+
# ADR 016: diagram sources live in docs/diagrams/*.mmd; rendered images are build output.
|
|
9
|
+
docs/static/diagrams/
|
|
10
|
+
# ADR 015: generated reference docs are committed on purpose (drift detection), so they
|
|
11
|
+
# are NOT ignored here — see docs/reference/.
|
|
12
|
+
|
|
13
|
+
# --- Python ---
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*.so
|
|
17
|
+
.Python
|
|
18
|
+
build/
|
|
19
|
+
dist/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.eggs/
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
env/
|
|
25
|
+
|
|
26
|
+
# Tooling caches
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.hypothesis/
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
htmlcov/
|
|
34
|
+
coverage.xml
|
|
35
|
+
.benchmarks/
|
|
36
|
+
.tox/
|
|
37
|
+
.nox/
|
|
38
|
+
|
|
39
|
+
# uv (ADR 013) — uv.lock IS committed; the cache is not.
|
|
40
|
+
.uv/
|
|
41
|
+
|
|
42
|
+
# --- Node / web (ADR 009) ---
|
|
43
|
+
node_modules/
|
|
44
|
+
.next/
|
|
45
|
+
out/
|
|
46
|
+
.turbo/
|
|
47
|
+
*.tsbuildinfo
|
|
48
|
+
.pnpm-store/
|
|
49
|
+
|
|
50
|
+
# --- Tauri (ADR 010) ---
|
|
51
|
+
apps/desktop/src-tauri/target/
|
|
52
|
+
apps/desktop/src-tauri/gen/
|
|
53
|
+
|
|
54
|
+
# --- Docs site (ADR 015) ---
|
|
55
|
+
docs/.docusaurus/
|
|
56
|
+
docs/build/
|
|
57
|
+
|
|
58
|
+
# --- Metrik runtime data ---
|
|
59
|
+
# The artifact store and blob store (ADR 006). Content-addressed, machine-generated,
|
|
60
|
+
# gigabytes. Never in git.
|
|
61
|
+
.metrik/
|
|
62
|
+
*.metrik-store/
|
|
63
|
+
# Local config may hold API keys (ADR 018).
|
|
64
|
+
metrik.local.toml
|
|
65
|
+
.env
|
|
66
|
+
.env.*
|
|
67
|
+
!.env.example
|
|
68
|
+
|
|
69
|
+
# Model weights and checkpoints — always too large, often license-encumbered (plan.md §5.7).
|
|
70
|
+
*.safetensors
|
|
71
|
+
*.gguf
|
|
72
|
+
*.onnx
|
|
73
|
+
*.pt
|
|
74
|
+
*.pth
|
|
75
|
+
*.bin
|
|
76
|
+
# ...except the tiny-random fixtures, which are committed on purpose (ADR 017).
|
|
77
|
+
!tests/fixtures/**
|
|
78
|
+
|
|
79
|
+
# --- Editors and OS ---
|
|
80
|
+
.vscode/
|
|
81
|
+
!.vscode/extensions.json
|
|
82
|
+
.idea/
|
|
83
|
+
*.swp
|
|
84
|
+
.DS_Store
|
|
85
|
+
Thumbs.db
|
|
86
|
+
desktop.ini
|
|
87
|
+
|
|
88
|
+
# --- Local agent/tooling settings ---
|
|
89
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metrik-explorer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Metrik Model Explorer — architecture, parameter, and memory analysis without a forward pass.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Asmodeus14/Metrik
|
|
6
|
+
Project-URL: Repository, https://github.com/Asmodeus14/Metrik
|
|
7
|
+
Project-URL: Issues, https://github.com/Asmodeus14/Metrik/issues
|
|
8
|
+
Author: The Metrik Authors
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: metrik-sdk==0.1.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# metrik-explorer
|
|
18
|
+
|
|
19
|
+
Architecture, parameter, and memory analysis — **without a forward pass, without torch, and
|
|
20
|
+
without a GPU**.
|
|
21
|
+
|
|
22
|
+
```console
|
|
23
|
+
$ metrik explore ./models/TinyLlama-1.1B
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Reads safetensors headers (a length-prefixed JSON blob) and `config.json`. **No tensor is ever
|
|
27
|
+
deserialized**, no torch is imported, and no forward pass happens.
|
|
28
|
+
|
|
29
|
+
One honest caveat: `ModelFingerprint.weight_digest` is a merkle over the weight files, because
|
|
30
|
+
identity has to be content-based — a benchmark result attached to a *name* rather than a digest
|
|
31
|
+
is unfalsifiable ([`artifacts.md`](../../docs/planning/artifacts.md) §6.1). So the command does
|
|
32
|
+
stream every weight byte through BLAKE3 once. It is I/O-bound, not compute-bound, and nothing
|
|
33
|
+
is held in memory, but it is not free on a 140 GB directory. Header parsing alone *is* free,
|
|
34
|
+
and that is what the structural analysis uses.
|
|
35
|
+
|
|
36
|
+
Emits four artifacts:
|
|
37
|
+
|
|
38
|
+
| Artifact | What it carries |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `ModelFingerprint` | identity: merkle over weight files, config digest, param split |
|
|
41
|
+
| `ModelGraph` | one node per tensor — **no edges**, declared as an omission |
|
|
42
|
+
| `GraphSummary` | role/dtype/depth breakdown plus evidenced observations |
|
|
43
|
+
| `MemoryEstimate` | analytic weights + KV cache, with stated assumptions |
|
|
44
|
+
|
|
45
|
+
## What it deliberately does not do
|
|
46
|
+
|
|
47
|
+
- **No `ParamStats`.** Per-tensor statistics need the weight data and a blob format that is
|
|
48
|
+
still an open question ([`artifacts.md`](../../docs/planning/artifacts.md) §8, question 1).
|
|
49
|
+
- **No graph edges.** Headers describe storage, not dataflow. A topology needs torch and a
|
|
50
|
+
forward pass, which is a different loader behind the same contract.
|
|
51
|
+
- **No measured numbers.** `MemoryEstimate` is analytic by type and can never contain one
|
|
52
|
+
([`artifacts.md`](../../docs/planning/artifacts.md) §5).
|
|
53
|
+
|
|
54
|
+
Role classification is name-based pattern matching, so `GraphSummary.classification_confidence`
|
|
55
|
+
always reports what fraction of parameters it could not classify. A confident breakdown of a
|
|
56
|
+
model the heuristic did not understand is worse than no breakdown.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# metrik-explorer
|
|
2
|
+
|
|
3
|
+
Architecture, parameter, and memory analysis — **without a forward pass, without torch, and
|
|
4
|
+
without a GPU**.
|
|
5
|
+
|
|
6
|
+
```console
|
|
7
|
+
$ metrik explore ./models/TinyLlama-1.1B
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Reads safetensors headers (a length-prefixed JSON blob) and `config.json`. **No tensor is ever
|
|
11
|
+
deserialized**, no torch is imported, and no forward pass happens.
|
|
12
|
+
|
|
13
|
+
One honest caveat: `ModelFingerprint.weight_digest` is a merkle over the weight files, because
|
|
14
|
+
identity has to be content-based — a benchmark result attached to a *name* rather than a digest
|
|
15
|
+
is unfalsifiable ([`artifacts.md`](../../docs/planning/artifacts.md) §6.1). So the command does
|
|
16
|
+
stream every weight byte through BLAKE3 once. It is I/O-bound, not compute-bound, and nothing
|
|
17
|
+
is held in memory, but it is not free on a 140 GB directory. Header parsing alone *is* free,
|
|
18
|
+
and that is what the structural analysis uses.
|
|
19
|
+
|
|
20
|
+
Emits four artifacts:
|
|
21
|
+
|
|
22
|
+
| Artifact | What it carries |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `ModelFingerprint` | identity: merkle over weight files, config digest, param split |
|
|
25
|
+
| `ModelGraph` | one node per tensor — **no edges**, declared as an omission |
|
|
26
|
+
| `GraphSummary` | role/dtype/depth breakdown plus evidenced observations |
|
|
27
|
+
| `MemoryEstimate` | analytic weights + KV cache, with stated assumptions |
|
|
28
|
+
|
|
29
|
+
## What it deliberately does not do
|
|
30
|
+
|
|
31
|
+
- **No `ParamStats`.** Per-tensor statistics need the weight data and a blob format that is
|
|
32
|
+
still an open question ([`artifacts.md`](../../docs/planning/artifacts.md) §8, question 1).
|
|
33
|
+
- **No graph edges.** Headers describe storage, not dataflow. A topology needs torch and a
|
|
34
|
+
forward pass, which is a different loader behind the same contract.
|
|
35
|
+
- **No measured numbers.** `MemoryEstimate` is analytic by type and can never contain one
|
|
36
|
+
([`artifacts.md`](../../docs/planning/artifacts.md) §5).
|
|
37
|
+
|
|
38
|
+
Role classification is name-based pattern matching, so `GraphSummary.classification_confidence`
|
|
39
|
+
always reports what fraction of parameters it could not classify. A confident breakdown of a
|
|
40
|
+
model the heuristic did not understand is worse than no breakdown.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "metrik-explorer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Metrik Model Explorer — architecture, parameter, and memory analysis without a forward pass."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "The Metrik Authors" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# metrik-sdk ONLY. Not metrik-core: the Explorer is a plugin with no privileged access, and
|
|
20
|
+
# the `sdk-dogfooding` contract in .importlinter fails the build if that stops being true.
|
|
21
|
+
#
|
|
22
|
+
# No torch and no safetensors library either. A safetensors header is a length-prefixed JSON
|
|
23
|
+
# blob, so the whole metadata path is stdlib -- which is what lets `metrik explore` run on a
|
|
24
|
+
# laptop with no ML stack installed, and makes it honest that no forward pass occurred.
|
|
25
|
+
dependencies = [
|
|
26
|
+
"metrik-sdk==0.1.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# The Explorer is discovered exactly the way a third-party plugin is: an entry point plus a
|
|
30
|
+
# manifest shipped as package data. It gets no privileged path into the registry, which is
|
|
31
|
+
# how we find out in Phase 2 whether plugin discovery actually works (plan.md Verification #2).
|
|
32
|
+
[project.entry-points."metrik.loaders"]
|
|
33
|
+
explorer = "metrik.explorer:Explorer"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Asmodeus14/Metrik"
|
|
37
|
+
Repository = "https://github.com/Asmodeus14/Metrik"
|
|
38
|
+
Issues = "https://github.com/Asmodeus14/Metrik/issues"
|
|
39
|
+
|
|
40
|
+
[tool.uv.sources]
|
|
41
|
+
metrik-sdk = { workspace = true }
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/metrik"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Model Explorer — architecture, parameter, and memory analysis with no forward pass.
|
|
2
|
+
|
|
3
|
+
``plan.md`` §5.1. Reads safetensors headers and ``config.json`` and nothing else, which is why
|
|
4
|
+
it needs neither torch nor a GPU, and why the claim "no forward pass occurred" is checkable
|
|
5
|
+
rather than asserted.
|
|
6
|
+
|
|
7
|
+
Imports ``metrik.sdk`` and nothing else from Metrik. That is the dogfooding contract
|
|
8
|
+
(``sdk-contracts.md`` §1): the built-in Explorer is a plugin with no privileged access, so if
|
|
9
|
+
it ever needs something the SDK does not expose, the SDK is broken and gets fixed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from metrik.explorer.producer import INFO, ExploreParams, Explorer
|
|
15
|
+
from metrik.explorer.run import ExploreResult, explore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _distribution_version(name: str) -> str:
|
|
19
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
return version(name)
|
|
23
|
+
except PackageNotFoundError: # pragma: no cover - only in an uninstalled source tree
|
|
24
|
+
return "0+unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#: Read from the installed distribution rather than hardcoded.
|
|
28
|
+
#:
|
|
29
|
+
#: Both existed for a while and drifted: a 0.1.0 build reported 0.0.1 from `metrik --version`,
|
|
30
|
+
#: found by installing the actual wheel rather than by any test. A version string is exactly
|
|
31
|
+
#: the kind of duplicate that stays wrong quietly, because nothing imports it in anger.
|
|
32
|
+
#:
|
|
33
|
+
#: The fallback covers a source checkout with nothing installed, where there is no metadata to
|
|
34
|
+
#: read and no released version to name.
|
|
35
|
+
__version__ = _distribution_version("metrik-explorer")
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"INFO",
|
|
39
|
+
"ExploreParams",
|
|
40
|
+
"ExploreResult",
|
|
41
|
+
"Explorer",
|
|
42
|
+
"__version__",
|
|
43
|
+
"explore",
|
|
44
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"explorer": {
|
|
3
|
+
"name": "explorer",
|
|
4
|
+
"kind": "model_loader",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"metrik_api": ">=1.0,<2.0",
|
|
7
|
+
"summary": "Architecture, parameter, and memory analysis from safetensors headers. No forward pass.",
|
|
8
|
+
"homepage": "https://github.com/Asmodeus14/Metrik",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"requires_device": [],
|
|
11
|
+
"requires_gradients": false,
|
|
12
|
+
"requires_calibration": false,
|
|
13
|
+
"requires_container": false,
|
|
14
|
+
"determinism": "deterministic",
|
|
15
|
+
"device_sensitive": false,
|
|
16
|
+
"writes_files": false,
|
|
17
|
+
"needs_network": false,
|
|
18
|
+
"executes_untrusted_code": false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Locating and digesting the files that make up a model on disk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from metrik.explorer.safetensors import SafetensorsHeader, read_header
|
|
11
|
+
from metrik.sdk import Digest, HashAlgo, UserError, content_hash, hash_stream
|
|
12
|
+
|
|
13
|
+
__all__ = ["ModelDir", "config_digest_of", "digest_file", "discover", "merkle_of"]
|
|
14
|
+
|
|
15
|
+
WEIGHT_SUFFIXES = (".safetensors",)
|
|
16
|
+
#: Formats we can see but not yet read. Named explicitly so the error can say which one.
|
|
17
|
+
UNSUPPORTED_SUFFIXES = {
|
|
18
|
+
".bin": "PyTorch pickle",
|
|
19
|
+
".pt": "PyTorch pickle",
|
|
20
|
+
".pth": "PyTorch pickle",
|
|
21
|
+
".gguf": "GGUF",
|
|
22
|
+
".onnx": "ONNX",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ModelDir:
|
|
28
|
+
"""A model directory, resolved and read."""
|
|
29
|
+
|
|
30
|
+
root: Path
|
|
31
|
+
headers: tuple[SafetensorsHeader, ...]
|
|
32
|
+
config: dict[str, Any]
|
|
33
|
+
config_path: Path | None
|
|
34
|
+
tokenizer_path: Path | None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def weight_files(self) -> tuple[Path, ...]:
|
|
38
|
+
return tuple(h.path for h in self.headers)
|
|
39
|
+
|
|
40
|
+
def source_stamp(self) -> tuple[tuple[str, int, int], ...]:
|
|
41
|
+
"""Cheap identity of the bytes on disk: (name, size, mtime_ns) per file, sorted.
|
|
42
|
+
|
|
43
|
+
Deliberately not a content digest. This feeds ``derivation_hash``, which must be
|
|
44
|
+
computable *before* running -- digesting gigabytes to decide whether to skip digesting
|
|
45
|
+
gigabytes is not a cache.
|
|
46
|
+
"""
|
|
47
|
+
paths = [*self.weight_files]
|
|
48
|
+
if self.config_path:
|
|
49
|
+
paths.append(self.config_path)
|
|
50
|
+
return tuple(sorted((p.name, p.stat().st_size, p.stat().st_mtime_ns) for p in paths))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def discover(uri: str) -> ModelDir:
|
|
54
|
+
"""Resolve a local path to a model directory and read every safetensors header.
|
|
55
|
+
|
|
56
|
+
Only local paths in v0.1. Hugging Face resolution is a separate loader behind the same
|
|
57
|
+
contract; keeping it out means this path has no network dependency and no cache semantics
|
|
58
|
+
to reason about.
|
|
59
|
+
"""
|
|
60
|
+
root = Path(uri).expanduser()
|
|
61
|
+
files: tuple[Path, ...]
|
|
62
|
+
if root.is_file() and root.suffix in WEIGHT_SUFFIXES:
|
|
63
|
+
root, files = root.parent, (root,)
|
|
64
|
+
elif root.is_dir():
|
|
65
|
+
files = tuple(sorted(p for p in root.iterdir() if p.suffix in WEIGHT_SUFFIXES))
|
|
66
|
+
else:
|
|
67
|
+
raise UserError(
|
|
68
|
+
f"{uri} is not a file or directory",
|
|
69
|
+
remediation="Pass a path to a model directory containing .safetensors files.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not files:
|
|
73
|
+
found = {p.suffix for p in root.iterdir() if p.is_file()} & set(UNSUPPORTED_SUFFIXES)
|
|
74
|
+
if found:
|
|
75
|
+
names = ", ".join(sorted(UNSUPPORTED_SUFFIXES[s] for s in found))
|
|
76
|
+
raise UserError(
|
|
77
|
+
f"{root} contains {names} weights, which this loader cannot read",
|
|
78
|
+
remediation=(
|
|
79
|
+
"Convert to safetensors, or wait for the format-specific loader. Metrik "
|
|
80
|
+
"reads safetensors headers without torch, which is why it is the one "
|
|
81
|
+
"supported format in v0.1."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
raise UserError(
|
|
85
|
+
f"no .safetensors files in {root}",
|
|
86
|
+
remediation="Check the path points at a model directory.",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
config_path = root / "config.json"
|
|
90
|
+
config: dict[str, Any] = {}
|
|
91
|
+
if config_path.is_file():
|
|
92
|
+
try:
|
|
93
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
95
|
+
raise UserError(
|
|
96
|
+
f"config.json in {root} is not readable JSON: {exc}",
|
|
97
|
+
remediation="Re-download the model, or remove the file to analyse weights only.",
|
|
98
|
+
) from exc
|
|
99
|
+
|
|
100
|
+
tokenizer = root / "tokenizer.json"
|
|
101
|
+
return ModelDir(
|
|
102
|
+
root=root,
|
|
103
|
+
headers=tuple(read_header(p) for p in files),
|
|
104
|
+
config=config,
|
|
105
|
+
config_path=config_path if config_path.is_file() else None,
|
|
106
|
+
tokenizer_path=tokenizer if tokenizer.is_file() else None,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def digest_file(path: Path) -> Digest:
|
|
111
|
+
with path.open("rb") as handle:
|
|
112
|
+
return hash_stream(handle)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def merkle_of(digests: list[tuple[str, Digest]]) -> Digest:
|
|
116
|
+
"""Combine per-file digests into one identity.
|
|
117
|
+
|
|
118
|
+
Sorted by path so the result does not depend on directory iteration order, and computed
|
|
119
|
+
over the *canonical form* of the (name, digest) pairs so it inherits the canonicalization
|
|
120
|
+
rules rather than inventing a second, subtly different, serialization.
|
|
121
|
+
"""
|
|
122
|
+
return content_hash([{"path": name, "digest": str(digest)} for name, digest in sorted(digests)])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def config_digest_of(config: dict[str, Any]) -> Digest:
|
|
126
|
+
"""Hash a parsed config.
|
|
127
|
+
|
|
128
|
+
Over the parsed object rather than the raw bytes, so that reformatting ``config.json``
|
|
129
|
+
(whitespace, key order) does not invalidate every cached analysis -- while a real change
|
|
130
|
+
to a value still does.
|
|
131
|
+
"""
|
|
132
|
+
return content_hash(config, algo=HashAlgo.BLAKE3)
|