nvdc 0.2.2__tar.gz → 0.2.4__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.
- {nvdc-0.2.2 → nvdc-0.2.4}/PKG-INFO +12 -3
- {nvdc-0.2.2 → nvdc-0.2.4}/README.md +8 -2
- {nvdc-0.2.2 → nvdc-0.2.4}/pyproject.toml +6 -1
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/__init__.py +1 -1
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/agent.py +18 -11
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/catalog.py +38 -11
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/cli.py +4 -1
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/coordinator.py +32 -9
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/house.py +1 -0
- nvdc-0.2.4/src/nvdc/inference.py +350 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/models.py +67 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/protocol.py +1 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/runtime.py +122 -5
- nvdc-0.2.4/src/nvdc/store.py +105 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/PKG-INFO +12 -3
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/SOURCES.txt +3 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/requires.txt +4 -0
- nvdc-0.2.4/tests/test_mlx_custody.py +146 -0
- nvdc-0.2.4/tests/test_ollama_autoinstall.py +83 -0
- nvdc-0.2.2/src/nvdc/inference.py +0 -148
- {nvdc-0.2.2 → nvdc-0.2.4}/setup.cfg +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/accounts.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/app.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/attestation.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/config.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/gpu.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/hardware.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/keys.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/payments.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/storage.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/update.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/wallet.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/web/index.html +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/dependency_links.txt +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/entry_points.txt +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/top_level.txt +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_accounts_auth.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_coordinator_security.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_house_node.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_model_attestation.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_persistence.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_self_serve.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_self_update.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_split_pricing.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_storage_money.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_stripe_webhook.py +0 -0
- {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_version_gate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nvdc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Bring your GPU onto the network: one command turns a GPU into a verifiable, OpenAI-compatible inference node.
|
|
5
5
|
Author: NVDC
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,9 +14,12 @@ Requires-Dist: cryptography>=42.0
|
|
|
14
14
|
Requires-Dist: nvidia-ml-py>=12.535.77
|
|
15
15
|
Requires-Dist: redis>=5.0
|
|
16
16
|
Requires-Dist: stripe>=9.0
|
|
17
|
+
Requires-Dist: zstandard>=0.22
|
|
17
18
|
Provides-Extra: attestation
|
|
18
19
|
Requires-Dist: nv-attestation-sdk>=2.7.0; extra == "attestation"
|
|
19
20
|
Requires-Dist: nv-local-gpu-verifier>=2.7.0; extra == "attestation"
|
|
21
|
+
Provides-Extra: mlx
|
|
22
|
+
Requires-Dist: mlx-lm>=0.22; extra == "mlx"
|
|
20
23
|
Provides-Extra: dev
|
|
21
24
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
25
|
|
|
@@ -127,8 +130,14 @@ client.chat.completions.create(model="llama3.1:8b",
|
|
|
127
130
|
## Models
|
|
128
131
|
|
|
129
132
|
The catalog is pinned to the **Ollama** library (reliable, known sizes; Ollama
|
|
130
|
-
also handles CUDA / Apple Metal / CPU offload). Each
|
|
131
|
-
|
|
133
|
+
also handles CUDA / Apple Metal / CPU offload). Each catalog entry also maps to
|
|
134
|
+
a canonical **MLX** artifact (4-bit, `mlx-community` on Hugging Face): run
|
|
135
|
+
`nvdc serve --backend mlx` and the client downloads the weights into its own
|
|
136
|
+
store (`~/.nvdc/models`), hashes them itself (full custody), re-verifies them
|
|
137
|
+
before every go-live, and serves them via a managed `mlx_lm.server` —
|
|
138
|
+
`pip install "nvdc[mlx]"` (Apple silicon; on Linux add `mlx[cuda]`).
|
|
139
|
+
Each node reports its memory budget and the UI marks every model
|
|
140
|
+
**Fits / Tight / Won't fit** against it:
|
|
132
141
|
|
|
133
142
|
- unified-memory systems (DGX Spark / GB10, Apple Silicon) → budget = system RAM
|
|
134
143
|
- dedicated-VRAM GPUs → budget = VRAM
|
|
@@ -105,8 +105,14 @@ client.chat.completions.create(model="llama3.1:8b",
|
|
|
105
105
|
## Models
|
|
106
106
|
|
|
107
107
|
The catalog is pinned to the **Ollama** library (reliable, known sizes; Ollama
|
|
108
|
-
also handles CUDA / Apple Metal / CPU offload). Each
|
|
109
|
-
|
|
108
|
+
also handles CUDA / Apple Metal / CPU offload). Each catalog entry also maps to
|
|
109
|
+
a canonical **MLX** artifact (4-bit, `mlx-community` on Hugging Face): run
|
|
110
|
+
`nvdc serve --backend mlx` and the client downloads the weights into its own
|
|
111
|
+
store (`~/.nvdc/models`), hashes them itself (full custody), re-verifies them
|
|
112
|
+
before every go-live, and serves them via a managed `mlx_lm.server` —
|
|
113
|
+
`pip install "nvdc[mlx]"` (Apple silicon; on Linux add `mlx[cuda]`).
|
|
114
|
+
Each node reports its memory budget and the UI marks every model
|
|
115
|
+
**Fits / Tight / Won't fit** against it:
|
|
110
116
|
|
|
111
117
|
- unified-memory systems (DGX Spark / GB10, Apple Silicon) → budget = system RAM
|
|
112
118
|
- dedicated-VRAM GPUs → budget = VRAM
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nvdc"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Bring your GPU onto the network: one command turns a GPU into a verifiable, OpenAI-compatible inference node."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -15,10 +15,15 @@ dependencies = [
|
|
|
15
15
|
"nvidia-ml-py>=12.535.77",
|
|
16
16
|
"redis>=5.0",
|
|
17
17
|
"stripe>=9.0",
|
|
18
|
+
# Ollama distributes its runtime as .tar.zst; used by the auto-installer.
|
|
19
|
+
"zstandard>=0.22",
|
|
18
20
|
]
|
|
19
21
|
|
|
20
22
|
[project.optional-dependencies]
|
|
21
23
|
attestation = ["nv-attestation-sdk>=2.7.0", "nv-local-gpu-verifier>=2.7.0"]
|
|
24
|
+
# Apple-silicon (and MLX-CUDA) inference with full client custody of weights.
|
|
25
|
+
# On Linux, additionally install the CUDA flavor: pip install "mlx[cuda]"
|
|
26
|
+
mlx = ["mlx-lm>=0.22"]
|
|
22
27
|
dev = ["pytest>=8.0"]
|
|
23
28
|
|
|
24
29
|
[project.scripts]
|
|
@@ -21,7 +21,6 @@ from .gpu import detect_gpu, detect_gpus, detect_interconnect
|
|
|
21
21
|
from .hardware import detect_hardware, machine_id as hw_machine_id
|
|
22
22
|
from .inference import Backend, make_backend
|
|
23
23
|
from .keys import Identity
|
|
24
|
-
from .models import local_ollama_commit
|
|
25
24
|
|
|
26
25
|
log = logging.getLogger("nvdc.agent")
|
|
27
26
|
|
|
@@ -215,15 +214,15 @@ class NodeAgent:
|
|
|
215
214
|
)
|
|
216
215
|
# Model identity commitment: the content digest of the artifact this
|
|
217
216
|
# node will serve. Signed into every work receipt (nvdc-work-v2) and
|
|
218
|
-
# checked by the coordinator against
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
# checked by the coordinator against public ground truth. For MLX the
|
|
218
|
+
# client itself hashed the weights it downloaded (full custody); for
|
|
219
|
+
# Ollama it is the content digest the local daemon reports.
|
|
220
|
+
self.model_commit = await self.backend.model_commitment(self.model)
|
|
221
|
+
if self.model_commit:
|
|
222
|
+
log.info("model commitment: %s -> %s", self.model, self.model_commit[:23] + "…")
|
|
223
|
+
elif getattr(self.backend, "name", "") in ("ollama", "mlx"):
|
|
224
|
+
log.warning("could not derive a model commitment for '%s'; "
|
|
225
|
+
"receipts will not carry a model identity", self.model)
|
|
227
226
|
profile = protocol.NodeProfile(
|
|
228
227
|
name=self.name,
|
|
229
228
|
models=[self.model],
|
|
@@ -243,6 +242,7 @@ class NodeAgent:
|
|
|
243
242
|
cluster=self.cluster,
|
|
244
243
|
private=self.private,
|
|
245
244
|
model_commit=self.model_commit,
|
|
245
|
+
backend=getattr(self.backend, "name", "ollama"),
|
|
246
246
|
agent_version=__version__,
|
|
247
247
|
)
|
|
248
248
|
await self._send(protocol.MSG_REGISTER, profile=protocol.node_profile_to_dict(profile))
|
|
@@ -392,4 +392,11 @@ async def serve(
|
|
|
392
392
|
private=private,
|
|
393
393
|
auto_update=auto_update,
|
|
394
394
|
)
|
|
395
|
-
|
|
395
|
+
try:
|
|
396
|
+
await agent.run_forever()
|
|
397
|
+
finally:
|
|
398
|
+
# Backends that manage a server subprocess (MLX) must not orphan it
|
|
399
|
+
# when the agent exits (Ctrl-C, drain, upgrade-required, errors).
|
|
400
|
+
close = getattr(backend, "close", None)
|
|
401
|
+
if callable(close):
|
|
402
|
+
close()
|
|
@@ -25,37 +25,63 @@ class Model:
|
|
|
25
25
|
size_gb: float # published download size of this tag
|
|
26
26
|
min_memory_gb: float # recommended resident memory for hot inference
|
|
27
27
|
description: str
|
|
28
|
+
# Hugging Face repo of the canonical MLX artifact for this model (4-bit,
|
|
29
|
+
# mlx-community). The coordinator derives MLX ground-truth digests from
|
|
30
|
+
# THIS mapping — never from a node-supplied repo. "" = no MLX artifact.
|
|
31
|
+
mlx_repo: str = ""
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
# Curated, popular, reliably-available tags from the Ollama library.
|
|
31
35
|
CATALOG: List[Model] = [
|
|
32
36
|
Model("gpt-oss:20b", "GPT-OSS 20B", "gpt-oss", "20B", 14.0, 16.0,
|
|
33
|
-
"OpenAI's open-weight model; strong general reasoning, runs on a single 16GB+ GPU."
|
|
37
|
+
"OpenAI's open-weight model; strong general reasoning, runs on a single 16GB+ GPU.",
|
|
38
|
+
mlx_repo="mlx-community/gpt-oss-20b-MXFP4-Q4"),
|
|
34
39
|
Model("gpt-oss:120b", "GPT-OSS 120B", "gpt-oss", "120B", 65.0, 80.0,
|
|
35
|
-
"OpenAI's large open-weight model; needs ~80GB (high-end / unified-memory systems)."
|
|
40
|
+
"OpenAI's large open-weight model; needs ~80GB (high-end / unified-memory systems).",
|
|
41
|
+
mlx_repo="mlx-community/gpt-oss-120b-MXFP4-Q4"),
|
|
36
42
|
Model("llama3.1:8b", "Llama 3.1 8B", "llama", "8B", 4.9, 8.0,
|
|
37
|
-
"Meta Llama 3.1 8B Instruct; excellent quality-to-size, great default."
|
|
43
|
+
"Meta Llama 3.1 8B Instruct; excellent quality-to-size, great default.",
|
|
44
|
+
mlx_repo="mlx-community/Meta-Llama-3.1-8B-Instruct-4bit"),
|
|
38
45
|
Model("llama3.1:70b", "Llama 3.1 70B", "llama", "70B", 43.0, 48.0,
|
|
39
|
-
"Meta Llama 3.1 70B Instruct; frontier-class quality, needs a large GPU."
|
|
46
|
+
"Meta Llama 3.1 70B Instruct; frontier-class quality, needs a large GPU.",
|
|
47
|
+
mlx_repo="mlx-community/Meta-Llama-3.1-70B-Instruct-4bit"),
|
|
48
|
+
Model("llama3.2:1b", "Llama 3.2 1B", "llama", "1B", 1.3, 2.0,
|
|
49
|
+
"Tiny Llama 3.2; runs almost anywhere, good for testing and edge.",
|
|
50
|
+
mlx_repo="mlx-community/Llama-3.2-1B-Instruct-4bit"),
|
|
40
51
|
Model("llama3.2:3b", "Llama 3.2 3B", "llama", "3B", 2.0, 4.0,
|
|
41
|
-
"Small, fast Llama 3.2; good for low-memory GPUs and edge."
|
|
52
|
+
"Small, fast Llama 3.2; good for low-memory GPUs and edge.",
|
|
53
|
+
mlx_repo="mlx-community/Llama-3.2-3B-Instruct-4bit"),
|
|
42
54
|
Model("qwen2.5:7b", "Qwen2.5 7B", "qwen", "7B", 4.7, 8.0,
|
|
43
|
-
"Alibaba Qwen2.5 7B; strong multilingual and coding."
|
|
55
|
+
"Alibaba Qwen2.5 7B; strong multilingual and coding.",
|
|
56
|
+
mlx_repo="mlx-community/Qwen2.5-7B-Instruct-4bit"),
|
|
44
57
|
Model("qwen2.5:32b", "Qwen2.5 32B", "qwen", "32B", 20.0, 24.0,
|
|
45
|
-
"Qwen2.5 32B; high quality, needs a 24GB+ GPU."
|
|
58
|
+
"Qwen2.5 32B; high quality, needs a 24GB+ GPU.",
|
|
59
|
+
mlx_repo="mlx-community/Qwen2.5-32B-Instruct-4bit"),
|
|
46
60
|
Model("deepseek-r1:7b", "DeepSeek-R1 7B", "deepseek", "7B", 4.7, 8.0,
|
|
47
|
-
"Distilled reasoning model with visible chain-of-thought."
|
|
61
|
+
"Distilled reasoning model with visible chain-of-thought.",
|
|
62
|
+
mlx_repo="mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"),
|
|
48
63
|
Model("deepseek-r1:70b", "DeepSeek-R1 70B", "deepseek", "70B", 43.0, 48.0,
|
|
49
64
|
"Large distilled reasoning model."),
|
|
50
65
|
Model("mistral:7b", "Mistral 7B", "mistral", "7B", 4.1, 6.0,
|
|
51
|
-
"Mistral 7B Instruct; fast, lightweight, widely used."
|
|
66
|
+
"Mistral 7B Instruct; fast, lightweight, widely used.",
|
|
67
|
+
mlx_repo="mlx-community/Mistral-7B-Instruct-v0.3-4bit"),
|
|
52
68
|
Model("gemma2:9b", "Gemma 2 9B", "gemma", "9B", 5.4, 9.0,
|
|
53
|
-
"Google Gemma 2 9B; balanced quality and size."
|
|
69
|
+
"Google Gemma 2 9B; balanced quality and size.",
|
|
70
|
+
mlx_repo="mlx-community/gemma-2-9b-it-4bit"),
|
|
54
71
|
Model("phi4:14b", "Phi-4 14B", "phi", "14B", 9.1, 12.0,
|
|
55
|
-
"Microsoft Phi-4; strong reasoning for its size."
|
|
72
|
+
"Microsoft Phi-4; strong reasoning for its size.",
|
|
73
|
+
mlx_repo="mlx-community/phi-4-4bit"),
|
|
56
74
|
]
|
|
57
75
|
|
|
58
76
|
|
|
77
|
+
def mlx_repo_for(model_id: str) -> str:
|
|
78
|
+
"""The canonical MLX artifact repo for a catalog model id ("" if none)."""
|
|
79
|
+
for m in CATALOG:
|
|
80
|
+
if m.id == model_id:
|
|
81
|
+
return m.mlx_repo
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
|
|
59
85
|
# Default reference market prices in USD per 1,000,000 output tokens. Used as a
|
|
60
86
|
# fallback when no miner is currently online serving a given model. Loosely
|
|
61
87
|
# tracks relative model size/cost; the live network price overrides these.
|
|
@@ -64,6 +90,7 @@ DEFAULT_PRICES = {
|
|
|
64
90
|
"gpt-oss:120b": 0.60,
|
|
65
91
|
"llama3.1:8b": 0.10,
|
|
66
92
|
"llama3.1:70b": 0.40,
|
|
93
|
+
"llama3.2:1b": 0.03,
|
|
67
94
|
"llama3.2:3b": 0.05,
|
|
68
95
|
"qwen2.5:7b": 0.10,
|
|
69
96
|
"qwen2.5:32b": 0.30,
|
|
@@ -163,7 +163,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
163
163
|
s.add_argument("--coordinator", "-c", default=DEFAULT_COORDINATOR_WS,
|
|
164
164
|
help=f"coordinator host/url (default: {DEFAULT_COORDINATOR_WS})")
|
|
165
165
|
s.add_argument("--name", "-n", default=os.uname().nodename, help="node display name")
|
|
166
|
-
s.add_argument("--backend", "-b", default="ollama",
|
|
166
|
+
s.add_argument("--backend", "-b", default="ollama",
|
|
167
|
+
choices=["ollama", "mlx", "echo"],
|
|
168
|
+
help="inference engine; 'mlx' downloads the canonical MLX "
|
|
169
|
+
"artifact into the client's own store (full custody)")
|
|
167
170
|
s.add_argument("--model", "-m", default="llama3.1:8b",
|
|
168
171
|
help="the single model this node commits to serving (hot-loaded)")
|
|
169
172
|
s.add_argument("--ollama-url", default="http://localhost:11434")
|
|
@@ -187,10 +187,13 @@ class Node:
|
|
|
187
187
|
self.machine_id = profile.get("machine_id", "") or self.id
|
|
188
188
|
self.cluster = profile.get("cluster", "") or ""
|
|
189
189
|
# Model identity: the node's claimed artifact digest, and the verdict
|
|
190
|
-
# from checking that claim against
|
|
190
|
+
# from checking that claim against public ground truth at registration
|
|
191
191
|
# ("verified" | "mismatch" | "unknown").
|
|
192
192
|
self.model_commit = str(profile.get("model_commit", "") or "")
|
|
193
193
|
self.model_verdict = "unknown"
|
|
194
|
+
# Inference engine; ground truth differs per backend (ollama registry
|
|
195
|
+
# manifest vs Hugging Face LFS digests for the catalog's MLX repo).
|
|
196
|
+
self.backend = str(profile.get("backend", "") or "ollama")
|
|
194
197
|
self.draining = False
|
|
195
198
|
self.pending: Dict[str, PendingRequest] = {}
|
|
196
199
|
self._send_lock = asyncio.Lock()
|
|
@@ -265,6 +268,7 @@ class Node:
|
|
|
265
268
|
"price_per_mtok": round(self.price_per_mtok, 6),
|
|
266
269
|
"price_per_mtok_input": round(self.price_per_mtok_input, 6),
|
|
267
270
|
"private": self.private,
|
|
271
|
+
"backend": self.backend,
|
|
268
272
|
"model_commit": self.model_commit,
|
|
269
273
|
"model_verdict": self.model_verdict,
|
|
270
274
|
"attested": self.attested,
|
|
@@ -511,24 +515,43 @@ def create_app(auth_token: str = "", require_attested: bool = False,
|
|
|
511
515
|
node.restore_stats(rec)
|
|
512
516
|
return
|
|
513
517
|
|
|
514
|
-
#
|
|
515
|
-
#
|
|
518
|
+
# Ground truth for model digests, cached so registration doesn't hammer
|
|
519
|
+
# the upstream sources (tags/repos are mutable, hence the TTL refresh).
|
|
516
520
|
_model_digest_cache: Dict[str, tuple] = {}
|
|
517
521
|
|
|
518
|
-
async def
|
|
519
|
-
hit = _model_digest_cache.get(
|
|
522
|
+
async def _cached(key: str, fetch) -> str:
|
|
523
|
+
hit = _model_digest_cache.get(key)
|
|
520
524
|
if hit and time.time() - hit[1] < 6 * 3600:
|
|
521
525
|
return hit[0]
|
|
522
|
-
digest = await
|
|
523
|
-
_model_digest_cache[
|
|
526
|
+
digest = await fetch()
|
|
527
|
+
_model_digest_cache[key] = (digest, time.time())
|
|
524
528
|
return digest
|
|
525
529
|
|
|
530
|
+
async def expected_model_digest(node: Node) -> str:
|
|
531
|
+
"""Authoritative digest for the node's model, per its backend."""
|
|
532
|
+
if node.backend == "mlx":
|
|
533
|
+
from .catalog import mlx_repo_for
|
|
534
|
+
repo = mlx_repo_for(node.model)
|
|
535
|
+
if not repo:
|
|
536
|
+
return "" # non-catalog model: no canonical MLX ground truth
|
|
537
|
+
|
|
538
|
+
async def fetch_mlx() -> str:
|
|
539
|
+
digests = await model_identity.fetch_hf_weights_digests(repo)
|
|
540
|
+
return model_identity.weights_commitment(digests) if digests else ""
|
|
541
|
+
|
|
542
|
+
return await _cached("mlx:" + repo, fetch_mlx)
|
|
543
|
+
if node.backend in ("api", "echo"):
|
|
544
|
+
return "" # no public artifact to anchor to
|
|
545
|
+
return await _cached(
|
|
546
|
+
"ollama:" + node.model,
|
|
547
|
+
lambda: model_identity.fetch_registry_digest(node.model))
|
|
548
|
+
|
|
526
549
|
async def _verify_node_model(node: Node) -> None:
|
|
527
|
-
"""Check the node's claimed artifact digest against
|
|
550
|
+
"""Check the node's claimed artifact digest against ground truth."""
|
|
528
551
|
if not node.model_commit:
|
|
529
552
|
return # no claim made -> verdict stays "unknown"
|
|
530
553
|
try:
|
|
531
|
-
expected = await expected_model_digest(node
|
|
554
|
+
expected = await expected_model_digest(node)
|
|
532
555
|
except Exception:
|
|
533
556
|
expected = ""
|
|
534
557
|
node.model_verdict = model_identity.model_verdict(node.model_commit, expected)
|
|
@@ -117,6 +117,7 @@ class HouseNode(Node):
|
|
|
117
117
|
"owner_account": identity.account_id,
|
|
118
118
|
"machine_id": "house-" + identity.account_id[-12:],
|
|
119
119
|
"cluster": "house",
|
|
120
|
+
"backend": "api",
|
|
120
121
|
"attestation": {
|
|
121
122
|
"supported": False, "verified": False, "mode": "none",
|
|
122
123
|
"reason": "house node: API-backed, no local GPU to attest",
|