dual billing; tracker to node model sharing

This commit is contained in:
Dobromir Popov
2026-07-06 17:31:11 +03:00
parent ccb69c41e3
commit 2e696be80f
14 changed files with 1092 additions and 41 deletions

View File

@@ -1,6 +1,6 @@
# US-044 — Tracker as model-file source; nodes download only their shard
Status: planned
Status: in progress
Priority: High (blocks multi-machine big-model serving; pairs with US-042)
Stage: Designed (grill remaining decisions before build)
@@ -56,10 +56,30 @@ What exists already (build on it, don't duplicate):
## Acceptance criteria
- Two-machine test: machine A (tracker + node, holds full snapshot) serves
- [x] Tracker can be started with `--models-dir PATH` / `MESHNET_MODELS_DIR`
and advertises a local model-file source in assignment responses when it has
a matching HF snapshot.
- [x] Tracker serves a tar stream containing only the safetensors files selected
for the assigned layer range plus config/tokenizer/index metadata.
- [x] Node downloader keeps exact-shard peers first, then races tracker model
sources against a HuggingFace `snapshot_download(..., allow_patterns=...)`
subset download, using the first successful source.
- [x] Real PyTorch model startup can use tracker `full_url` sources to fetch
the full local snapshot over LAN before `from_pretrained`, so local-network
testing no longer has to pull from HuggingFace first.
- [ ] Two-machine test: machine A (tracker + node, holds full snapshot) serves
layers 0k; machine B joins with no model and receives **only** the files
for its assigned range from A — nothing fetched from HF
- Machine B's resident memory scales with its shard size, not model size
- Checksums verified end-to-end; corrupted transfer falls back cleanly
- Single-node/full-model flows unchanged
- `python -m pytest` passes from repo root
- [ ] Machine B's resident memory scales with its shard size, not model size
- [ ] Checksums verified end-to-end; corrupted transfer falls back cleanly
- [x] Single-node/full-model flows unchanged
- [ ] `python -m pytest` passes from repo root
## Implementation notes
- 2026-07-06: Added the tracker/node download path. For immediate Qwen3.6-35B
LAN testing, real PyTorch nodes fetch the full snapshot from the tracker via
`full_url` and race HuggingFace as fallback. Remaining hard half is true
partial model materialization: the backend can prefer a downloaded local
model directory, but Transformers still needs a `meta`-device load path that
materializes only assigned layers.

View File

@@ -0,0 +1,60 @@
# US-045 — Dual-rate billing: separate input and output token prices
Status: in progress
Priority: High (billing correctness before friends test; providers all price this way)
Stage: Designed
## Context
Today the ledger has one `price_per_1k_tokens` per model, and the two proxy
paths don't even agree on what they count:
- **Non-streaming** bills `usage.total_tokens` (prompt + completion) at the
blended rate (`_billable_non_stream_tokens`).
- **Streaming** bills `min(observed output deltas, reported total)` — output
only in practice (`_billable_stream_tokens`).
- The HF pricing refresher (issue 23) averages a provider's input/output
rates 50/50 (`blended_price_per_1k_tokens`), which misprices asymmetric
models — e.g. Qwen3.6-35B-A3B on deepinfra is $0.15/1M in, $0.95/1M out.
Decision (user, 2026-07-06): charge **both** input and output tokens, at
**two separate rates**, same as other providers.
## Design
1. **`BillingLedger`** stores `{model: (input_per_1k, output_per_1k)}`.
- `set_prices(model, input_per_1k, output_per_1k)` (new);
`set_price(model, p)` keeps working and sets both.
- `prices_for(model) -> (input, output)` (new); `price_for(model)` returns
the blended average for back-compat (estimators/logs).
- `charge_request(...)` gains keyword `input_tokens`/`output_tokens`; when
provided, `cost = in_rate·in/1k + out_rate·out/1k` and the event records
the split. Without them, legacy behavior (blended × total) — old events
and gossip replicas replay unchanged (`cost` stays the applied field).
2. **Token counting** (`server.py`):
- Non-stream: prefer `usage.prompt_tokens`/`completion_tokens`; fall back
to content estimates (`_estimate_prompt_tokens`, observed completion),
capped by `max_tokens` bounds as today.
- Stream (direct + relay): output = observed deltas as today; input =
`usage.prompt_tokens` when a usage chunk appears, else the prompt
estimate from the request body. `_stream_line_tokens` returns the parsed
usage triple instead of just the total.
3. **Presets**: `input_price_per_1k_tokens` / `output_price_per_1k_tokens`
(dual keys win; `price_per_1k_tokens` alone still means "both rates").
Qwen3.6-35B-A3B: input 0.00012, output 0.00076 (80% of deepinfra).
4. **HF refresher**: applies 80% of each side separately via `set_prices`
(all alias keys); change log keeps recording the blended pair for history
continuity.
5. **Spend cap** (`--max-charge-per-request`): estimate =
`in_rate·prompt_estimate + out_rate·completion_limit`.
## Acceptance criteria
- Streamed and non-streamed requests for the same exchange bill the same
split (input charged in both)
- A model with asymmetric provider rates bills input and output differently;
`usage_for` / billing events expose the split
- Old persisted billing events replay byte-identically (balances unchanged)
- HF refresh sets both rates from the marketplace row, not the average
- Spend cap uses the dual rates
- `python -m pytest` passes from repo root