fix LAN discovery/connection
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# US-019 — Binary data plane and optional peer weight transfer
|
||||
|
||||
Status: needs-triage
|
||||
Priority: Low
|
||||
Stage: Design parking lot
|
||||
|
||||
## Context
|
||||
|
||||
The current project focus is inference democratization: let small GPU owners contribute useful compute and let users run inference on models larger than one host can serve alone. Weight distribution is useful, but it is secondary to low-latency distributed inference.
|
||||
|
||||
Recent findings:
|
||||
|
||||
- HuggingFace already handles initial model origin distribution well enough for the first working version.
|
||||
- The inference-critical path is activation transfer between shard nodes, not torrenting model files.
|
||||
- Torrent/content-addressed transfer is a good future fit for model weights, shard cache replication, fine-tuned models, and offline/local swarm behavior.
|
||||
- Torrenting is not a good fit for activation traffic because activations are latency-sensitive, ordered, session-specific binary streams.
|
||||
|
||||
## Design note
|
||||
|
||||
Keep the tracker as the control plane:
|
||||
|
||||
- node registration
|
||||
- heartbeats
|
||||
- route selection
|
||||
- model/shard manifests
|
||||
- peer/checksum metadata
|
||||
|
||||
Keep binary payloads on the data plane:
|
||||
|
||||
- direct node-to-node activation transfer where reachable
|
||||
- relay/QUIC/WSS fallback where direct transport is unavailable
|
||||
- future peer weight transfer as content-addressed blobs or pieces
|
||||
|
||||
## Potential future direction
|
||||
|
||||
For inference traffic:
|
||||
|
||||
- Prefer direct binary transport with backpressure.
|
||||
- Use raw binary activation frames rather than JSON/base64.
|
||||
- Preserve tensor metadata out-of-band: shape, dtype, session, chunk index, encoding.
|
||||
- Consider QUIC or a mature NAT-friendly transport for direct-when-possible, relay-when-needed behavior.
|
||||
|
||||
For model weights:
|
||||
|
||||
- Keep HuggingFace as default origin and fallback.
|
||||
- Add peer cache transfer only as an optimization.
|
||||
- Consider a real content-addressed/torrent-like library or sidecar for weight blobs.
|
||||
- Store manifests and checksums in tracker state so peers can verify exact shard contents.
|
||||
- Prefer piece/chunk transfer with resume and hash verification over one giant tarball.
|
||||
|
||||
## Not in scope now
|
||||
|
||||
- Replacing HuggingFace as the primary model origin.
|
||||
- Building a full BitTorrent/IPFS/libp2p subsystem.
|
||||
- Routing activation traffic through a torrent protocol.
|
||||
- Making peer weight transfer mandatory for node startup.
|
||||
|
||||
## Acceptance criteria for a future implementation issue
|
||||
|
||||
- [ ] Activation transfer remains binary end-to-end and avoids JSON/base64 payloads.
|
||||
- [ ] Tracker does not proxy large binary payloads except as an explicit fallback path.
|
||||
- [ ] Weight transfer, if added, is optional and falls back to HuggingFace.
|
||||
- [ ] Weight pieces are content-addressed and checksum-verified.
|
||||
- [ ] The design preserves low-latency inference as the primary objective.
|
||||
|
||||
## Comments
|
||||
|
||||
Created as a low-priority design parking-lot item after discussing inference democratization versus weight distribution. Do not pick up for implementation until the core public tracker, relay, and binary activation path are stable.
|
||||
4
_DEV_NOTES.md
Normal file
4
_DEV_NOTES.md
Normal file
@@ -0,0 +1,4 @@
|
||||
win
|
||||
.venv/bin/meshnet-node start --tracker http://192.168.0.179:8081 --model Qwen/Qwen2.5-0.5B-Instruct --shard-start 20
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ def _run_node(cfg: dict) -> None:
|
||||
wallet_path=Path(cfg["wallet_path"]) if cfg.get("wallet_path") else None,
|
||||
cache_dir=Path(cfg["download_dir"]) if cfg.get("download_dir") else None,
|
||||
host=cfg.get("host", "0.0.0.0"),
|
||||
advertise_host=cfg.get("advertise_host"),
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"\nERROR: {exc}", file=sys.stderr, flush=True)
|
||||
@@ -86,6 +87,8 @@ def _cmd_default(args) -> int:
|
||||
overrides["port"] = args.port
|
||||
if args.host:
|
||||
overrides["host"] = args.host
|
||||
if args.advertise_host:
|
||||
overrides["advertise_host"] = args.advertise_host
|
||||
|
||||
if overrides:
|
||||
cfg = merge_cli_overrides(cfg, **overrides)
|
||||
@@ -143,6 +146,10 @@ def _cmd_start(args) -> int:
|
||||
cfg = dict(DEFAULTS)
|
||||
cfg["tracker_url"] = args.tracker
|
||||
cfg["port"] = args.port
|
||||
if args.model_id is None and "/" in args.model:
|
||||
cfg["model_hf_repo"] = args.model
|
||||
cfg["model_name"] = args.model.split("/")[-1]
|
||||
else:
|
||||
cfg["model_name"] = args.model
|
||||
cfg["quantization"] = args.quantization
|
||||
cfg["host"] = args.host
|
||||
@@ -212,6 +219,7 @@ def main() -> None:
|
||||
parser.add_argument("--shard-end", type=int, metavar="N", help="Pin shard end layer")
|
||||
parser.add_argument("--port", type=int, metavar="N", help="Port to listen on")
|
||||
parser.add_argument("--host", metavar="ADDR", help="Interface to bind (default 0.0.0.0)")
|
||||
parser.add_argument("--advertise-host", metavar="ADDR", help="Host/IP advertised to the tracker")
|
||||
parser.add_argument("--no-tui", action="store_true", help="Plain-text output (no rich dashboard)")
|
||||
parser.add_argument("--compact", action="store_true", help="Single-line status output")
|
||||
parser.add_argument("--reset-config", action="store_true", help="Re-run wizard even if config exists")
|
||||
|
||||
@@ -8,40 +8,43 @@ from .server import TrackerServer
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="meshnet-tracker",
|
||||
description="Distributed Inference Network node registry and route selection",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
start_cmd = subparsers.add_parser("start", help="Start the tracker server")
|
||||
start_cmd.add_argument("--host", default="127.0.0.1", help="Host interface to listen on")
|
||||
start_cmd.add_argument("--port", type=int, default=8080, help="Port to listen on")
|
||||
start_cmd.add_argument(
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--host", default="0.0.0.0", help="Host interface to listen on")
|
||||
common.add_argument("--port", type=int, default=8080, help="Port to listen on")
|
||||
common.add_argument(
|
||||
"--heartbeat-timeout",
|
||||
type=float,
|
||||
default=30.0,
|
||||
help="Seconds before a node is removed from the registry after missed heartbeat",
|
||||
)
|
||||
start_cmd.add_argument(
|
||||
common.add_argument(
|
||||
"--cluster-peers",
|
||||
default="",
|
||||
help="Comma-separated URLs of peer tracker nodes (enables Raft cluster mode)",
|
||||
)
|
||||
start_cmd.add_argument(
|
||||
common.add_argument(
|
||||
"--self-url",
|
||||
default=None,
|
||||
help="This tracker's own URL as seen by peers (auto-derived from --host/--port if omitted)",
|
||||
)
|
||||
start_cmd.add_argument(
|
||||
common.add_argument(
|
||||
"--relay-url",
|
||||
default=None,
|
||||
help="Public ws(s):// relay URL advertised to nodes, for example wss://ai.neuron.d-popov.com/ws",
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="meshnet-tracker",
|
||||
description="Distributed Inference Network node registry and route selection",
|
||||
parents=[common],
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
subparsers.add_parser("start", help="Start the tracker server", parents=[common])
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "start":
|
||||
if args.command in {None, "start"}:
|
||||
cluster_peers = [u.strip() for u in args.cluster_peers.split(",") if u.strip()]
|
||||
server = TrackerServer(
|
||||
host=args.host,
|
||||
|
||||
@@ -354,3 +354,76 @@ def test_legacy_start_subcommand_accepted(monkeypatch):
|
||||
|
||||
# Exited (either 0 or via KeyboardInterrupt caught in _cmd_start)
|
||||
# The important thing is no unhandled exception from arg parsing
|
||||
|
||||
|
||||
def test_legacy_start_treats_repo_model_as_model_id(monkeypatch):
|
||||
"""`meshnet-node start --model org/repo` enters the HF model startup path."""
|
||||
from meshnet_node.cli import main
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_run_startup(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
class _FakeNode:
|
||||
chat_completion_count = 0
|
||||
def stop(self): pass
|
||||
return _FakeNode()
|
||||
|
||||
monkeypatch.setattr(sys, "argv", [
|
||||
"meshnet-node", "start",
|
||||
"--tracker", "http://192.168.0.179:8081",
|
||||
"--model", "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
"--port", "0",
|
||||
])
|
||||
|
||||
with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup):
|
||||
with patch("time.sleep", side_effect=KeyboardInterrupt):
|
||||
try:
|
||||
main()
|
||||
except SystemExit as exc:
|
||||
assert exc.code == 0
|
||||
|
||||
assert captured["model"] == "Qwen2.5-0.5B-Instruct"
|
||||
assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct"
|
||||
|
||||
|
||||
def test_default_cli_passes_advertise_host(monkeypatch):
|
||||
"""The documented no-subcommand LAN flag reaches startup."""
|
||||
from meshnet_node.cli import main
|
||||
|
||||
saved = {
|
||||
"model_hf_repo": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
"model_name": "Qwen2.5-0.5B-Instruct",
|
||||
"quantization": "nf4",
|
||||
"tracker_url": "http://localhost:8080",
|
||||
"wallet_path": "",
|
||||
"download_dir": "",
|
||||
"port": 7000,
|
||||
"host": "0.0.0.0",
|
||||
}
|
||||
captured = {}
|
||||
|
||||
def fake_run_startup(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
class _FakeNode:
|
||||
chat_completion_count = 0
|
||||
def stop(self): pass
|
||||
return _FakeNode()
|
||||
|
||||
monkeypatch.setattr(sys, "argv", [
|
||||
"meshnet-node",
|
||||
"--tracker", "http://192.168.0.179:8081",
|
||||
"--advertise-host", "192.168.0.42",
|
||||
"--no-tui",
|
||||
])
|
||||
|
||||
with patch("meshnet_node.config.load_config", return_value=saved):
|
||||
with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup):
|
||||
with patch("meshnet_node.dashboard.run_dashboard", side_effect=KeyboardInterrupt):
|
||||
try:
|
||||
main()
|
||||
except SystemExit as exc:
|
||||
assert exc.code == 0
|
||||
|
||||
assert captured["tracker_url"] == "http://192.168.0.179:8081"
|
||||
assert captured["advertise_host"] == "192.168.0.42"
|
||||
|
||||
Reference in New Issue
Block a user