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.
Files changed (47) hide show
  1. {nvdc-0.2.2 → nvdc-0.2.4}/PKG-INFO +12 -3
  2. {nvdc-0.2.2 → nvdc-0.2.4}/README.md +8 -2
  3. {nvdc-0.2.2 → nvdc-0.2.4}/pyproject.toml +6 -1
  4. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/__init__.py +1 -1
  5. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/agent.py +18 -11
  6. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/catalog.py +38 -11
  7. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/cli.py +4 -1
  8. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/coordinator.py +32 -9
  9. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/house.py +1 -0
  10. nvdc-0.2.4/src/nvdc/inference.py +350 -0
  11. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/models.py +67 -0
  12. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/protocol.py +1 -0
  13. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/runtime.py +122 -5
  14. nvdc-0.2.4/src/nvdc/store.py +105 -0
  15. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/PKG-INFO +12 -3
  16. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/SOURCES.txt +3 -0
  17. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/requires.txt +4 -0
  18. nvdc-0.2.4/tests/test_mlx_custody.py +146 -0
  19. nvdc-0.2.4/tests/test_ollama_autoinstall.py +83 -0
  20. nvdc-0.2.2/src/nvdc/inference.py +0 -148
  21. {nvdc-0.2.2 → nvdc-0.2.4}/setup.cfg +0 -0
  22. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/accounts.py +0 -0
  23. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/app.py +0 -0
  24. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/attestation.py +0 -0
  25. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/config.py +0 -0
  26. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/gpu.py +0 -0
  27. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/hardware.py +0 -0
  28. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/keys.py +0 -0
  29. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/payments.py +0 -0
  30. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/storage.py +0 -0
  31. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/update.py +0 -0
  32. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/wallet.py +0 -0
  33. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc/web/index.html +0 -0
  34. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/dependency_links.txt +0 -0
  35. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/entry_points.txt +0 -0
  36. {nvdc-0.2.2 → nvdc-0.2.4}/src/nvdc.egg-info/top_level.txt +0 -0
  37. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_accounts_auth.py +0 -0
  38. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_coordinator_security.py +0 -0
  39. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_house_node.py +0 -0
  40. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_model_attestation.py +0 -0
  41. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_persistence.py +0 -0
  42. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_self_serve.py +0 -0
  43. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_self_update.py +0 -0
  44. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_split_pricing.py +0 -0
  45. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_storage_money.py +0 -0
  46. {nvdc-0.2.2 → nvdc-0.2.4}/tests/test_stripe_webhook.py +0 -0
  47. {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.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 node reports its memory
131
- budget and the UI marks every model **Fits / Tight / Won't fit** against it:
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 node reports its memory
109
- budget and the UI marks every model **Fits / Tight / Won't fit** against it:
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.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]
@@ -1,3 +1,3 @@
1
1
  """nvdc: bring your GPU onto the network as a verifiable, OpenAI-compatible inference node."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -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 the public registry.
219
- if getattr(self.backend, "name", "") == "ollama":
220
- self.model_commit = await local_ollama_commit(
221
- self.model, getattr(self.backend, "base_url", "http://localhost:11434"))
222
- if self.model_commit:
223
- log.info("model commitment: %s -> %s", self.model, self.model_commit[:23] + "…")
224
- else:
225
- log.warning("could not derive a model commitment for '%s'; "
226
- "receipts will not carry a model identity", self.model)
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
- await agent.run_forever()
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", choices=["ollama", "echo"])
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 the public registry at registration
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
- # Registry ground truth for model digests, cached so registration doesn't
515
- # hammer registry.ollama.ai (tags are mutable, hence the TTL refresh).
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 expected_model_digest(model: str) -> str:
519
- hit = _model_digest_cache.get(model)
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 model_identity.fetch_registry_digest(model)
523
- _model_digest_cache[model] = (digest, time.time())
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 the registry."""
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.model)
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",