dual billing; tracker to node model sharing
This commit is contained in:
@@ -24,6 +24,18 @@ DEFAULT_BILLING_DB_PATH = "billing.sqlite"
|
||||
NODE_REVENUE_SHARE = 0.90 # nodes 90% / protocol cut 10% (ADR-0015)
|
||||
|
||||
|
||||
def _normalize_rates(value: "float | tuple[float, float] | dict") -> tuple[float, float]:
|
||||
"""Coerce a price spec into an (input_per_1k, output_per_1k) pair."""
|
||||
if isinstance(value, dict):
|
||||
base = value.get("price")
|
||||
inp = value.get("input", base)
|
||||
out = value.get("output", base)
|
||||
return (float(inp), float(out))
|
||||
if isinstance(value, (tuple, list)):
|
||||
return (float(value[0]), float(value[1]))
|
||||
return (float(value), float(value))
|
||||
|
||||
|
||||
class BillingLedger:
|
||||
"""Thread-safe USDT ledger with SQLite persistence and event replication."""
|
||||
|
||||
@@ -33,13 +45,17 @@ class BillingLedger:
|
||||
self,
|
||||
db_path: str | None = None,
|
||||
*,
|
||||
prices: dict[str, float] | None = None,
|
||||
prices: dict[str, float | tuple[float, float]] | None = None,
|
||||
default_price_per_1k: float = DEFAULT_PRICE_PER_1K_TOKENS,
|
||||
starting_credit: float = DEFAULT_STARTING_CREDIT,
|
||||
node_share: float = NODE_REVENUE_SHARE,
|
||||
) -> None:
|
||||
self._db_path = db_path
|
||||
self._prices = dict(prices) if prices else {}
|
||||
# US-045: per-model (input_per_1k, output_per_1k). A bare float in
|
||||
# ``prices`` sets both rates (legacy single-rate models).
|
||||
self._prices: dict[str, tuple[float, float]] = {
|
||||
model: _normalize_rates(value) for model, value in (prices or {}).items()
|
||||
}
|
||||
self._default_price_per_1k = default_price_per_1k
|
||||
self._starting_credit = starting_credit
|
||||
self._node_share = node_share
|
||||
@@ -62,11 +78,23 @@ class BillingLedger:
|
||||
# ---- pricing ----
|
||||
|
||||
def price_for(self, model: str) -> float:
|
||||
return self._prices.get(model, self._default_price_per_1k)
|
||||
"""Blended (average) per-1k rate — kept for estimators and history logs."""
|
||||
rates = self._prices.get(model)
|
||||
if rates is None:
|
||||
return self._default_price_per_1k
|
||||
return (rates[0] + rates[1]) / 2.0
|
||||
|
||||
def prices_for(self, model: str) -> tuple[float, float]:
|
||||
"""(input_per_1k, output_per_1k) for a model (US-045)."""
|
||||
return self._prices.get(model, (self._default_price_per_1k, self._default_price_per_1k))
|
||||
|
||||
def set_price(self, model: str, price_per_1k: float) -> None:
|
||||
"""Legacy single-rate setter — applies the same rate to input and output."""
|
||||
self.set_prices(model, price_per_1k, price_per_1k)
|
||||
|
||||
def set_prices(self, model: str, input_per_1k: float, output_per_1k: float) -> None:
|
||||
with self._lock:
|
||||
self._prices[model] = price_per_1k
|
||||
self._prices[model] = (float(input_per_1k), float(output_per_1k))
|
||||
|
||||
# ---- local operations (create + apply + log an event) ----
|
||||
|
||||
@@ -130,9 +158,17 @@ class BillingLedger:
|
||||
model: str,
|
||||
total_tokens: int,
|
||||
node_work: list[tuple[str | None, int]],
|
||||
*,
|
||||
input_tokens: int | None = None,
|
||||
output_tokens: int | None = None,
|
||||
) -> dict:
|
||||
"""Debit the client and split the fee 90/10.
|
||||
|
||||
With ``input_tokens``/``output_tokens`` (US-045) the cost is
|
||||
``input·in_rate + output·out_rate``; without them, legacy behavior —
|
||||
``total_tokens`` at the blended rate. Replayed/gossiped events apply
|
||||
their recorded ``cost``, so old events are unaffected either way.
|
||||
|
||||
``node_work`` is ``[(wallet_address | None, work_units), ...]`` for the
|
||||
nodes that served the request. Work units of nodes without a wallet
|
||||
accrue to the protocol cut — there is nowhere to pay them out.
|
||||
@@ -140,7 +176,14 @@ class BillingLedger:
|
||||
request is then rejected by ``has_funds`` (post-pay drift, standard
|
||||
metered-billing behavior).
|
||||
"""
|
||||
cost = self.price_for(model) * max(0, total_tokens) / 1000.0
|
||||
if input_tokens is not None or output_tokens is not None:
|
||||
in_rate, out_rate = self.prices_for(model)
|
||||
in_tokens = max(0, input_tokens or 0)
|
||||
out_tokens = max(0, output_tokens or 0)
|
||||
cost = (in_tokens * in_rate + out_tokens * out_rate) / 1000.0
|
||||
total_tokens = in_tokens + out_tokens
|
||||
else:
|
||||
cost = self.price_for(model) * max(0, total_tokens) / 1000.0
|
||||
total_work = sum(max(0, w) for _, w in node_work)
|
||||
node_pool = cost * self._node_share
|
||||
shares: dict[str, float] = {}
|
||||
@@ -165,6 +208,9 @@ class BillingLedger:
|
||||
"api_key": api_key,
|
||||
"model": model,
|
||||
"total_tokens": total_tokens,
|
||||
**({"input_tokens": max(0, input_tokens or 0),
|
||||
"output_tokens": max(0, output_tokens or 0)}
|
||||
if (input_tokens is not None or output_tokens is not None) else {}),
|
||||
"cost": cost,
|
||||
"shares": shares,
|
||||
"protocol_amount": protocol_amount,
|
||||
|
||||
@@ -214,12 +214,18 @@ def main() -> None:
|
||||
"enables GET /v1/pricing/hf/history)"
|
||||
),
|
||||
)
|
||||
common.add_argument(
|
||||
"--hf-pricing-refresh-interval",
|
||||
type=float,
|
||||
default=86400.0,
|
||||
help="Seconds between dynamic pricing refresh passes (default: daily)",
|
||||
)
|
||||
common.add_argument(
|
||||
"--hf-pricing-refresh-interval",
|
||||
type=float,
|
||||
default=86400.0,
|
||||
help="Seconds between dynamic pricing refresh passes (default: daily)",
|
||||
)
|
||||
common.add_argument(
|
||||
"--models-dir",
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help="Local HuggingFace snapshot root advertised as tracker model-file source (default: MESHNET_MODELS_DIR)",
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="meshnet-tracker",
|
||||
@@ -276,8 +282,9 @@ def main() -> None:
|
||||
args.hf_pricing_log_db
|
||||
or (DEFAULT_HF_PRICING_LOG_DB_PATH if args.enable_hf_pricing else None)
|
||||
),
|
||||
hf_pricing_refresh_interval=args.hf_pricing_refresh_interval,
|
||||
)
|
||||
hf_pricing_refresh_interval=args.hf_pricing_refresh_interval,
|
||||
models_dir=args.models_dir,
|
||||
)
|
||||
port = server.start()
|
||||
print(f"meshnet-tracker listening on http://{args.host}:{port}", flush=True)
|
||||
try:
|
||||
|
||||
172
packages/tracker/meshnet_tracker/model_files.py
Normal file
172
packages/tracker/meshnet_tracker/model_files.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Helpers for serving layer-scoped model files from tracker-local snapshots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
INDEX_FILENAME = "model.safetensors.index.json"
|
||||
|
||||
_LAYER_RE = re.compile(
|
||||
r"(?:^|\.)"
|
||||
r"(?:model\.layers|layers|h|blocks|decoder\.layers|encoder\.layers)"
|
||||
r"\.(\d+)(?:\.|$)"
|
||||
)
|
||||
|
||||
_METADATA_FILENAMES = {
|
||||
INDEX_FILENAME,
|
||||
"config.json",
|
||||
"generation_config.json",
|
||||
"preprocessor_config.json",
|
||||
"special_tokens_map.json",
|
||||
"tokenizer.json",
|
||||
"tokenizer.model",
|
||||
"tokenizer_config.json",
|
||||
"vocab.json",
|
||||
"merges.txt",
|
||||
"added_tokens.json",
|
||||
}
|
||||
|
||||
_METADATA_PREFIXES = ("config.", "tokenizer.", "tokenizer_", "vocab.")
|
||||
|
||||
_HEAD_MARKERS = ("embed", "embedding", "embed_tokens", "wte", "wpe")
|
||||
|
||||
_TAIL_EXACT = {
|
||||
"lm_head.weight",
|
||||
"lm_head.bias",
|
||||
"model.norm.weight",
|
||||
"model.norm.bias",
|
||||
"transformer.ln_f.weight",
|
||||
"transformer.ln_f.bias",
|
||||
"decoder.final_layer_norm.weight",
|
||||
"decoder.final_layer_norm.bias",
|
||||
}
|
||||
|
||||
_TAIL_MARKERS = (".lm_head.", ".norm.", ".ln_f.", ".final_layer_norm.")
|
||||
|
||||
|
||||
def snapshot_dir_for_repo(models_dir: Path, repo_id: str) -> Path | None:
|
||||
"""Return the most likely local HF snapshot directory for *repo_id*."""
|
||||
candidates = [
|
||||
models_dir / repo_id,
|
||||
models_dir / repo_id.replace("/", "--"),
|
||||
models_dir / f"models--{repo_id.replace('/', '--')}",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if (candidate / "snapshots").is_dir():
|
||||
snapshots = sorted(p for p in (candidate / "snapshots").iterdir() if p.is_dir())
|
||||
if snapshots:
|
||||
return snapshots[-1]
|
||||
if candidate.is_dir():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def files_for_layer_range(snapshot_dir: Path, shard_start: int, shard_end: int) -> list[str]:
|
||||
"""Select files needed to load a conservative safetensors shard subset."""
|
||||
return select_safetensors_files_for_layers(snapshot_dir, shard_start, shard_end)
|
||||
|
||||
|
||||
def select_safetensors_files_for_layers(
|
||||
model_dir: str | Path,
|
||||
start_layer: int,
|
||||
end_layer: int,
|
||||
*,
|
||||
total_layers: int | None = None,
|
||||
) -> list[str]:
|
||||
if start_layer < 0:
|
||||
raise ValueError("start_layer must be non-negative")
|
||||
if end_layer < start_layer:
|
||||
raise ValueError("end_layer must be greater than or equal to start_layer")
|
||||
|
||||
root = Path(model_dir)
|
||||
index_path = root / INDEX_FILENAME
|
||||
try:
|
||||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return sorted(p.name for p in root.glob("*.safetensors") if p.is_file())
|
||||
|
||||
weight_map = index.get("weight_map")
|
||||
if not isinstance(weight_map, dict):
|
||||
raise ValueError(f"{INDEX_FILENAME} must contain a weight_map object")
|
||||
|
||||
inferred_total_layers = total_layers if total_layers is not None else _read_total_layers(root)
|
||||
selected = _metadata_files(root)
|
||||
|
||||
for tensor_name, rel_file in weight_map.items():
|
||||
if not isinstance(tensor_name, str) or not isinstance(rel_file, str):
|
||||
continue
|
||||
if _tensor_belongs_to_range(tensor_name, start_layer, end_layer, inferred_total_layers):
|
||||
selected.add(_normalise_relative_file(rel_file))
|
||||
|
||||
return sorted(rel for rel in selected if (root / rel).is_file())
|
||||
|
||||
|
||||
def _tensor_belongs_to_range(
|
||||
tensor_name: str,
|
||||
start_layer: int,
|
||||
end_layer: int,
|
||||
total_layers: int | None,
|
||||
) -> bool:
|
||||
layer = _layer_index(tensor_name)
|
||||
if layer is not None:
|
||||
return start_layer <= layer <= end_layer
|
||||
if start_layer == 0 and _is_head_tensor(tensor_name):
|
||||
return True
|
||||
if total_layers is not None and end_layer >= total_layers - 1 and _is_tail_tensor(tensor_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _layer_index(tensor_name: str) -> int | None:
|
||||
match = _LAYER_RE.search(tensor_name)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def _is_head_tensor(tensor_name: str) -> bool:
|
||||
lowered = tensor_name.lower()
|
||||
return any(marker in lowered for marker in _HEAD_MARKERS)
|
||||
|
||||
|
||||
def _is_tail_tensor(tensor_name: str) -> bool:
|
||||
lowered = tensor_name.lower()
|
||||
return lowered in _TAIL_EXACT or any(marker in lowered for marker in _TAIL_MARKERS)
|
||||
|
||||
|
||||
def _metadata_files(root: Path) -> set[str]:
|
||||
files = {INDEX_FILENAME}
|
||||
for path in root.iterdir():
|
||||
if not path.is_file():
|
||||
continue
|
||||
name = path.name
|
||||
if name in _METADATA_FILENAMES or name.startswith(_METADATA_PREFIXES):
|
||||
files.add(name)
|
||||
return files
|
||||
|
||||
|
||||
def _read_total_layers(root: Path) -> int | None:
|
||||
config_path = root / "config.json"
|
||||
if not config_path.exists():
|
||||
return None
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
return _layers_from_config(config)
|
||||
|
||||
|
||||
def _layers_from_config(config: dict[str, Any]) -> int | None:
|
||||
for key in ("num_hidden_layers", "num_layers", "n_layer", "n_layers"):
|
||||
value = config.get(key)
|
||||
if isinstance(value, int) and value > 0:
|
||||
return value
|
||||
text_config = config.get("text_config")
|
||||
if isinstance(text_config, dict):
|
||||
return _layers_from_config(text_config)
|
||||
return None
|
||||
|
||||
|
||||
def _normalise_relative_file(rel_file: str) -> str:
|
||||
path = Path(rel_file)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise ValueError(f"unsafe relative file in {INDEX_FILENAME}: {rel_file}")
|
||||
return path.as_posix()
|
||||
@@ -28,12 +28,14 @@ import json
|
||||
import os
|
||||
import socketserver
|
||||
import sqlite3
|
||||
import tarfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .accounts import DEFAULT_ACCOUNTS_DB_PATH, AccountStore
|
||||
@@ -43,6 +45,7 @@ from .billing import DEFAULT_BILLING_DB_PATH, BillingLedger
|
||||
from .calibration import DEFAULT_CALIBRATION_DB_PATH, ToplocCalibrationStore
|
||||
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH, HfPricingLog, refresh_preset_price
|
||||
from .gossip import NodeGossip
|
||||
from .model_files import files_for_layer_range, snapshot_dir_for_repo
|
||||
from .raft import RaftNode
|
||||
|
||||
|
||||
@@ -118,6 +121,14 @@ def _clone_model_presets(presets: dict[str, dict]) -> dict[str, dict]:
|
||||
|
||||
DEFAULT_VRAM_BYTES = 8 * 1024 * 1024 * 1024
|
||||
DEFAULT_RAM_BYTES = 16 * 1024 * 1024 * 1024
|
||||
|
||||
|
||||
def _snapshot_regular_files(snapshot_dir: Path) -> list[str]:
|
||||
return sorted(
|
||||
path.relative_to(snapshot_dir).as_posix()
|
||||
for path in snapshot_dir.rglob("*")
|
||||
if path.is_file()
|
||||
)
|
||||
DEFAULT_QUANTIZATIONS = ["bfloat16"]
|
||||
DEFAULT_BENCHMARK_TOKENS_PER_SEC = 1.0
|
||||
# US-039/US-040 — single source of truth for the credit defaults (referenced by
|
||||
@@ -1008,8 +1019,25 @@ def _relay_http_request(
|
||||
}
|
||||
|
||||
|
||||
def _stream_line_tokens(line: bytes) -> tuple[int, int | None]:
|
||||
"""Token accounting for one SSE line: (observed delta, reported total or None)."""
|
||||
def _usage_split(payload: dict) -> dict | None:
|
||||
"""Parse a usage block into {"prompt", "completion", "total"} (ints or None)."""
|
||||
usage = payload.get("usage")
|
||||
if not isinstance(usage, dict):
|
||||
return None
|
||||
|
||||
def _num(key: str) -> int | None:
|
||||
value = usage.get(key)
|
||||
return int(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
return {
|
||||
"prompt": _num("prompt_tokens"),
|
||||
"completion": _num("completion_tokens"),
|
||||
"total": _usage_total_tokens(payload),
|
||||
}
|
||||
|
||||
|
||||
def _stream_line_tokens(line: bytes) -> tuple[int, dict | None]:
|
||||
"""Token accounting for one SSE line: (observed output delta, usage split or None)."""
|
||||
if not line.startswith(b"data:"):
|
||||
return 0, None
|
||||
payload = line[5:].strip()
|
||||
@@ -1019,7 +1047,49 @@ def _stream_line_tokens(line: bytes) -> tuple[int, int | None]:
|
||||
chunk_payload = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
return 1, None
|
||||
return _observed_stream_tokens(chunk_payload), _usage_total_tokens(chunk_payload)
|
||||
return _observed_stream_tokens(chunk_payload), _usage_split(chunk_payload)
|
||||
|
||||
|
||||
def _stream_billable_split(
|
||||
observed_output: int, usage: dict | None, request_body: dict
|
||||
) -> tuple[int, int]:
|
||||
"""(input_tokens, output_tokens) for a streamed response (US-045).
|
||||
|
||||
Output: observed deltas, capped by reported completion count when the
|
||||
stream carried a usage chunk. Input: reported prompt count, else the
|
||||
prompt estimate from the request body.
|
||||
"""
|
||||
prompt = (usage or {}).get("prompt")
|
||||
completion = (usage or {}).get("completion")
|
||||
total = (usage or {}).get("total")
|
||||
if prompt is None:
|
||||
prompt = _estimate_prompt_tokens(request_body) or 0
|
||||
if completion is None and total is not None:
|
||||
completion = max(0, total - prompt)
|
||||
return max(0, prompt), _billable_stream_tokens(observed_output, completion)
|
||||
|
||||
|
||||
def _billable_non_stream_split(payload: dict, request_body: dict) -> tuple[int, int]:
|
||||
"""(input_tokens, output_tokens) for a buffered response (US-045).
|
||||
|
||||
Prefers the response usage block; falls back to content estimates.
|
||||
Completion stays capped by the request's max-tokens bound, as before.
|
||||
"""
|
||||
usage = _usage_split(payload)
|
||||
prompt = (usage or {}).get("prompt")
|
||||
completion = (usage or {}).get("completion")
|
||||
if prompt is None:
|
||||
prompt = _estimate_prompt_tokens(request_body) or 0
|
||||
if completion is None:
|
||||
total = (usage or {}).get("total")
|
||||
if total is not None:
|
||||
completion = max(0, total - prompt)
|
||||
else:
|
||||
completion = _observed_non_stream_completion_tokens(payload)
|
||||
limit = _requested_completion_token_limit(request_body)
|
||||
if limit is not None:
|
||||
completion = min(completion, limit)
|
||||
return max(0, prompt), max(0, completion)
|
||||
|
||||
|
||||
def _find_pinned_route(
|
||||
@@ -1598,6 +1668,7 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
toploc_calibration_gate_min_hardware_profiles: int = 1,
|
||||
toploc_backend: Any | None = None,
|
||||
hf_pricing_log: "HfPricingLog | None" = None,
|
||||
models_dir: Path | None = None,
|
||||
) -> None:
|
||||
super().__init__(addr, handler)
|
||||
self.registry = registry
|
||||
@@ -1628,6 +1699,7 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
self.toploc_calibration_gate_min_hardware_profiles = toploc_calibration_gate_min_hardware_profiles
|
||||
self.toploc_backend = toploc_backend
|
||||
self.hf_pricing_log: HfPricingLog | None = hf_pricing_log
|
||||
self.models_dir = models_dir
|
||||
|
||||
|
||||
class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
@@ -1800,6 +1872,8 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
self._handle_network_map()
|
||||
elif parsed.path == "/v1/models":
|
||||
self._handle_models()
|
||||
elif parsed.path == "/v1/model-files/download":
|
||||
self._handle_model_files_download(parsed)
|
||||
elif parsed.path.startswith("/v1/coverage/"):
|
||||
model = urllib.parse.unquote(parsed.path.removeprefix("/v1/coverage/"))
|
||||
self._handle_coverage(model)
|
||||
@@ -2482,8 +2556,16 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
model: str,
|
||||
total_tokens: int,
|
||||
node_work: list[tuple[str | None, int]],
|
||||
*,
|
||||
input_tokens: int | None = None,
|
||||
output_tokens: int | None = None,
|
||||
) -> None:
|
||||
"""Charge a completed request against the billing ledger (ADR-0015)."""
|
||||
"""Charge a completed request against the billing ledger (ADR-0015).
|
||||
|
||||
With ``input_tokens``/``output_tokens`` the ledger bills each side at
|
||||
its own rate (US-045); ``total_tokens`` alone falls back to the
|
||||
blended rate.
|
||||
"""
|
||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||
if server.billing is None or api_key is None:
|
||||
return
|
||||
@@ -2501,11 +2583,15 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
adjusted.append((wallet, work))
|
||||
node_work = adjusted
|
||||
try:
|
||||
event = server.billing.charge_request(api_key, model, total_tokens, node_work)
|
||||
event = server.billing.charge_request(
|
||||
api_key, model, total_tokens, node_work,
|
||||
input_tokens=input_tokens, output_tokens=output_tokens,
|
||||
)
|
||||
print(
|
||||
f"[tracker] billed api_key=…{api_key[-6:]}: model={model!r} "
|
||||
f"tokens={total_tokens} cost={event['cost']:.6f} USDT "
|
||||
f"shares={event['shares']}",
|
||||
f"tokens={event['total_tokens']} "
|
||||
f"(in={event.get('input_tokens', '?')} out={event.get('output_tokens', '?')}) "
|
||||
f"cost={event['cost']:.6f} USDT shares={event['shares']}",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -3699,9 +3785,83 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"model": resolved_name,
|
||||
"model_layers_end": required_end,
|
||||
"peers": peers,
|
||||
"model_sources": self._model_sources(
|
||||
resolved_name,
|
||||
preset,
|
||||
shard_start,
|
||||
shard_end,
|
||||
),
|
||||
**({"hf_repo": preset["hf_repo"]} if "hf_repo" in preset else {}),
|
||||
})
|
||||
|
||||
def _model_sources(self, model: str, preset: dict, shard_start: int, shard_end: int) -> list[dict]:
|
||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||
hf_repo = preset.get("hf_repo")
|
||||
if not server.models_dir or not isinstance(hf_repo, str) or not hf_repo:
|
||||
return []
|
||||
snapshot_dir = snapshot_dir_for_repo(server.models_dir, hf_repo)
|
||||
if snapshot_dir is None:
|
||||
return []
|
||||
files = files_for_layer_range(snapshot_dir, shard_start, shard_end)
|
||||
if not files:
|
||||
return []
|
||||
host = self.headers.get("Host") or f"{self.server.server_address[0]}:{self.server.server_address[1]}"
|
||||
base_url = f"http://{host}"
|
||||
query = urllib.parse.urlencode({
|
||||
"model": model,
|
||||
"shard_start": shard_start,
|
||||
"shard_end": shard_end,
|
||||
})
|
||||
full_query = urllib.parse.urlencode({"model": model, "full": 1})
|
||||
return [{
|
||||
"type": "tracker",
|
||||
"endpoint": base_url,
|
||||
"url": f"{base_url}/v1/model-files/download?{query}",
|
||||
"full_url": f"{base_url}/v1/model-files/download?{full_query}",
|
||||
"files": files,
|
||||
}]
|
||||
|
||||
def _handle_model_files_download(self, parsed: urllib.parse.ParseResult) -> None:
|
||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||
if server.models_dir is None:
|
||||
self._send_json(404, {"error": "tracker model-file source is not enabled"})
|
||||
return
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
model = params.get("model", [""])[0]
|
||||
resolved_name, preset = _resolve_model_preset(server.model_presets, model)
|
||||
if preset is None:
|
||||
self._send_json(404, {"error": f"unknown model preset: {model!r}"})
|
||||
return
|
||||
hf_repo = preset.get("hf_repo")
|
||||
if not isinstance(hf_repo, str) or not hf_repo:
|
||||
self._send_json(404, {"error": f"model preset has no hf_repo: {resolved_name!r}"})
|
||||
return
|
||||
snapshot_dir = snapshot_dir_for_repo(server.models_dir, hf_repo)
|
||||
if snapshot_dir is None:
|
||||
self._send_json(404, {"error": f"local snapshot not found for {hf_repo}"})
|
||||
return
|
||||
full_download = params.get("full", ["0"])[0] in {"1", "true", "yes"}
|
||||
if full_download:
|
||||
rel_files = _snapshot_regular_files(snapshot_dir)
|
||||
else:
|
||||
try:
|
||||
shard_start = int(params.get("shard_start", [""])[0])
|
||||
shard_end = int(params.get("shard_end", [""])[0])
|
||||
except ValueError:
|
||||
self._send_json(400, {"error": "shard_start and shard_end must be integers"})
|
||||
return
|
||||
rel_files = files_for_layer_range(snapshot_dir, shard_start, shard_end)
|
||||
if not rel_files:
|
||||
self._send_json(404, {"error": "no local files matched the assigned shard"})
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/x-tar")
|
||||
self.send_header("X-Meshnet-Model-Source", "tracker")
|
||||
self.end_headers()
|
||||
with tarfile.open(fileobj=self.wfile, mode="w|") as archive:
|
||||
for rel in rel_files:
|
||||
archive.add(snapshot_dir / rel, arcname=rel)
|
||||
|
||||
def _handle_network_assign(self, parsed: urllib.parse.ParseResult):
|
||||
"""Assign a new node to fill the biggest uncovered shard gap across HF-model nodes.
|
||||
|
||||
@@ -3771,6 +3931,12 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"gap_found": True,
|
||||
"price_per_token": 0.0,
|
||||
"deployment": _deployment_summary(all_nodes, preset),
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name),
|
||||
preset,
|
||||
shard_start,
|
||||
shard_end,
|
||||
),
|
||||
})
|
||||
return
|
||||
|
||||
@@ -3842,7 +4008,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
# Capacity: use the same 80%-of-memory rule as registered node planning.
|
||||
total_l = best_num_layers
|
||||
memory_mb = vram_mb if vram_mb > 0 else ram_mb
|
||||
_resolved_name, best_preset = _resolve_model_preset(server.model_presets, str(best_repo))
|
||||
resolved_name, best_preset = _resolve_model_preset(server.model_presets, str(best_repo))
|
||||
if memory_mb > 0:
|
||||
max_layers = _max_layers_for_memory(memory_mb, total_l, best_preset)
|
||||
elif device == "cuda" and vram_mb >= 8192:
|
||||
@@ -3855,11 +4021,18 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
self._send_json(200, {
|
||||
"hf_repo": best_repo,
|
||||
"model": resolved_name,
|
||||
"shard_start": shard_start,
|
||||
"shard_end": shard_end,
|
||||
"num_layers": total_l,
|
||||
"gap_found": gap_found,
|
||||
"price_per_token": 0.0,
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name or best_repo),
|
||||
best_preset or {"hf_repo": best_repo},
|
||||
shard_start,
|
||||
shard_end,
|
||||
),
|
||||
})
|
||||
|
||||
def _handle_route(self, parsed: urllib.parse.ParseResult):
|
||||
@@ -4053,6 +4226,7 @@ class TrackerServer:
|
||||
hf_pricing_log_db: str | None = None,
|
||||
hf_pricing_refresh_interval: float = 86400.0,
|
||||
hf_pricing_fetch_html: Any | None = None,
|
||||
models_dir: str | Path | None = None,
|
||||
) -> None:
|
||||
self._host = host
|
||||
self._requested_port = port
|
||||
@@ -4140,6 +4314,8 @@ class TrackerServer:
|
||||
self._enable_hf_pricing = enable_hf_pricing
|
||||
self._hf_pricing_refresh_interval = hf_pricing_refresh_interval
|
||||
self._hf_pricing_fetch_html = hf_pricing_fetch_html
|
||||
raw_models_dir = models_dir if models_dir is not None else os.environ.get("MESHNET_MODELS_DIR")
|
||||
self._models_dir = Path(raw_models_dir).expanduser() if raw_models_dir else None
|
||||
self._hf_pricing_stop = threading.Event()
|
||||
self._hf_pricing_thread: threading.Thread | None = None
|
||||
self.port: int | None = None
|
||||
@@ -4178,6 +4354,7 @@ class TrackerServer:
|
||||
toploc_calibration_gate_min_hardware_profiles=self._toploc_calibration_gate_min_hardware_profiles,
|
||||
toploc_backend=self._toploc_backend,
|
||||
hf_pricing_log=self._hf_pricing_log,
|
||||
models_dir=self._models_dir,
|
||||
)
|
||||
self.port = self._server.server_address[1]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user