-cpu flag

This commit is contained in:
Dobromir Popov
2026-07-09 08:19:15 +03:00
parent 4ed585bf54
commit 9ec4ca9ce1
8 changed files with 121 additions and 11 deletions

View File

@@ -60,13 +60,13 @@ Windows equivalent: `install.ps1` with the same flags.
| Flag | Meaning | | Flag | Meaning |
|------|---------| |------|---------|
| *(none)* | Auto-detect hardware, print detected profile, proceed with best match (interactive confirm unless `--yes`) | | *(none)* | Auto-detect hardware, print detected profile, proceed with best match (interactive confirm unless `--yes`) |
| `--cpu` | Skip GPU wheels entirely; install CPU PyTorch and register as a CPU node — even if a discrete GPU is present | | `--cpu` | Installer: CPU PyTorch wheel. **`meshnet-node --cpu` (implemented):** force CPU inference and CPU shard assignment even if a GPU is present |
| `--gpu` | Install and verify a GPU runtime; **fail hard** if GPU execution cannot be confirmed after install | | `--gpu` | Install and verify a GPU runtime; **fail hard** if GPU execution cannot be confirmed after install (installer only — not implemented on `meshnet-node` yet) |
| `--yes` | Skip interactive confirm; for headless installs and future web GUI orchestration | | `--yes` | Skip interactive confirm; for headless installs and future web GUI orchestration |
These flags set **intent** for the install environment. They do not replace runtime Installer flags set install-time intent. At runtime, `meshnet-node` auto-uses GPU when
hardware detection in `meshnet_node.hardware.detect_hardware()` — that profile still CUDA works; pass `--cpu` to ignore it. Hardware metadata (GPU name/VRAM) is still
feeds tracker registration and routing. detected for diagnostics.
### v1 install pipeline ### v1 install pipeline
@@ -243,7 +243,7 @@ Not in Ralph yet. See
| Hardware-aware + learned routing | **Working** (telemetry cleanup open) | US-027+, AH-024 | | Hardware-aware + learned routing | **Working** (telemetry cleanup open) | US-027+, AH-024 |
| Zero port-forwarding (relay) | **Working** (streamed relay chat open) | US-017, US-029, US-036 | | Zero port-forwarding (relay) | **Working** (streamed relay chat open) | US-017, US-029, US-036 |
| OpenAI-compatible API | **Working** | US-005 | | OpenAI-compatible API | **Working** | US-005 |
| Mining-style node CLI + wizard | **Working** (installer not built) | US-016 | | Mining-style node CLI + wizard | **Working** (`--cpu` forces CPU mode) | US-016 |
| Billing + devnet USDT | **Working** | US-031…033, alpha-hardening | | Billing + devnet USDT | **Working** | US-031…033, alpha-hardening |
| Fraud / TOPLOC / reputation | **Engineering done** (calibration ops pending) | AH-006…010, AH-021 | | Fraud / TOPLOC / reputation | **Engineering done** (calibration ops pending) | AH-006…010, AH-021 |
| Sharded per-node KV cache | **Implemented — GPU verify pending** | AH-025, ADR-0022 | | Sharded per-node KV cache | **Implemented — GPU verify pending** | AH-025, ADR-0022 |

View File

@@ -69,6 +69,7 @@ def _run_node(cfg: dict) -> None:
torch_threads=cfg.get("torch_threads"), torch_threads=cfg.get("torch_threads"),
torch_interop_threads=cfg.get("torch_interop_threads"), torch_interop_threads=cfg.get("torch_interop_threads"),
node_name=cfg.get("node_name"), node_name=cfg.get("node_name"),
force_cpu=bool(cfg.get("force_cpu", False)),
) )
except Exception as exc: except Exception as exc:
print(f"\nERROR: {exc}", file=sys.stderr, flush=True) print(f"\nERROR: {exc}", file=sys.stderr, flush=True)
@@ -174,6 +175,8 @@ def _cmd_default(args) -> int:
overrides["torch_threads"] = args.torch_threads overrides["torch_threads"] = args.torch_threads
if getattr(args, "torch_interop_threads", None) is not None: if getattr(args, "torch_interop_threads", None) is not None:
overrides["torch_interop_threads"] = args.torch_interop_threads overrides["torch_interop_threads"] = args.torch_interop_threads
if getattr(args, "cpu", False):
overrides["force_cpu"] = True
if overrides: if overrides:
cfg = merge_cli_overrides(cfg, **overrides) cfg = merge_cli_overrides(cfg, **overrides)
@@ -276,6 +279,7 @@ def _cmd_start(args) -> int:
torch_threads=getattr(args, "torch_threads", None), torch_threads=getattr(args, "torch_threads", None),
torch_interop_threads=getattr(args, "torch_interop_threads", None), torch_interop_threads=getattr(args, "torch_interop_threads", None),
node_name=cfg.get("node_name"), node_name=cfg.get("node_name"),
force_cpu=getattr(args, "cpu", False),
) )
except Exception as exc: except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr, flush=True) print(f"ERROR: {exc}", file=sys.stderr, flush=True)
@@ -332,6 +336,8 @@ def main() -> None:
help="Set PyTorch intra-op CPU worker threads") help="Set PyTorch intra-op CPU worker threads")
parser.add_argument("--torch-interop-threads", type=int, metavar="N", parser.add_argument("--torch-interop-threads", type=int, metavar="N",
help="Set PyTorch inter-op CPU worker threads") help="Set PyTorch inter-op CPU worker threads")
parser.add_argument("--cpu", action="store_true",
help="Force CPU inference even when a GPU is available")
parser.add_argument("--debug", action="store_true", help="Enable verbose node debug logging") parser.add_argument("--debug", action="store_true", help="Enable verbose node debug logging")
parser.add_argument("--no-tui", action="store_true", help="Plain-text output (no rich dashboard)") 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("--compact", action="store_true", help="Single-line status output")
@@ -372,6 +378,8 @@ def main() -> None:
help="Set PyTorch intra-op CPU worker threads") help="Set PyTorch intra-op CPU worker threads")
start_cmd.add_argument("--torch-interop-threads", type=int, metavar="N", start_cmd.add_argument("--torch-interop-threads", type=int, metavar="N",
help="Set PyTorch inter-op CPU worker threads") help="Set PyTorch inter-op CPU worker threads")
start_cmd.add_argument("--cpu", action="store_true",
help="Force CPU inference even when a GPU is available")
start_cmd.add_argument("--debug", action="store_true", help="Enable verbose node debug logging") start_cmd.add_argument("--debug", action="store_true", help="Enable verbose node debug logging")
start_cmd.add_argument("--tracker-source-disabled", action="store_true", start_cmd.add_argument("--tracker-source-disabled", action="store_true",
help="Skip tracker/peer model-file sources and download from HuggingFace directly") help="Skip tracker/peer model-file sources and download from HuggingFace directly")

View File

@@ -171,6 +171,18 @@ def _gpu_inventory_profile(gpu: dict | None, ram_mb: int) -> dict | None:
return profile return profile
def with_forced_cpu(hw: dict) -> dict:
"""Return a hardware profile forced to CPU execution.
Keeps detected GPU metadata for diagnostics and tracker registration context,
but clears CUDA availability so startup and the model backend stay on CPU.
"""
forced = dict(hw)
forced["device"] = "cpu"
forced["cuda_available"] = False
return forced
def detect_hardware() -> dict: def detect_hardware() -> dict:
"""Detect GPU model and available VRAM. Returns hardware profile dict.""" """Detect GPU model and available VRAM. Returns hardware profile dict."""
ram_mb = _detect_ram_mb() ram_mb = _detect_ram_mb()

View File

@@ -212,6 +212,7 @@ class TorchModelShard:
shard_end: int, shard_end: int,
quantization: Quantization = "auto", quantization: Quantization = "auto",
cache_dir: Path | None = None, cache_dir: Path | None = None,
force_cpu: bool = False,
) -> None: ) -> None:
if shard_start < 0 or shard_end < 0 or shard_start > shard_end: if shard_start < 0 or shard_end < 0 or shard_start > shard_end:
raise ValueError("shard_start must be <= shard_end and non-negative") raise ValueError("shard_start must be <= shard_end and non-negative")
@@ -229,7 +230,10 @@ class TorchModelShard:
) from exc ) from exc
self.torch = torch self.torch = torch
self.device = torch.device("cuda" if _torch_cuda_is_executable(torch) else "cpu") if force_cpu:
self.device = torch.device("cpu")
else:
self.device = torch.device("cuda" if _torch_cuda_is_executable(torch) else "cpu")
load_source = str(cache_dir) if cache_dir is not None and (cache_dir / "config.json").exists() else model_id load_source = str(cache_dir) if cache_dir is not None and (cache_dir / "config.json").exists() else model_id
quant_config, dtype, uses_quantized_weights = _model_load_plan( quant_config, dtype, uses_quantized_weights = _model_load_plan(
AutoConfig, AutoConfig,
@@ -682,8 +686,11 @@ def load_torch_shard(
shard_end: int, shard_end: int,
quantization: Quantization = "auto", quantization: Quantization = "auto",
cache_dir: Path | None = None, cache_dir: Path | None = None,
force_cpu: bool = False,
) -> TorchModelShard: ) -> TorchModelShard:
return TorchModelShard(model_id, shard_start, shard_end, quantization, cache_dir) return TorchModelShard(
model_id, shard_start, shard_end, quantization, cache_dir, force_cpu=force_cpu
)
def _total_layers_for_local_snapshot(auto_config: Any, load_source: str) -> int | None: def _total_layers_for_local_snapshot(auto_config: Any, load_source: str) -> int | None:

View File

@@ -15,7 +15,7 @@ from pathlib import Path
from typing import Any from typing import Any
from .downloader import compute_shard_checksum, download_shard from .downloader import compute_shard_checksum, download_shard
from .hardware import detect_hardware, benchmark_throughput_checked from .hardware import detect_hardware, benchmark_throughput_checked, with_forced_cpu
from .model_catalog import model_metadata_for from .model_catalog import model_metadata_for
from .relay_bridge import RelayHttpBridge, peer_id_from_wallet from .relay_bridge import RelayHttpBridge, peer_id_from_wallet
from .server import StubNodeServer from .server import StubNodeServer
@@ -640,6 +640,7 @@ def run_startup(
torch_threads: int | None = None, torch_threads: int | None = None,
torch_interop_threads: int | None = None, torch_interop_threads: int | None = None,
node_name: str | None = None, node_name: str | None = None,
force_cpu: bool = False,
) -> StubNodeServer | TorchNodeServer: ) -> StubNodeServer | TorchNodeServer:
"""Execute the full startup sequence and return a running node server. """Execute the full startup sequence and return a running node server.
@@ -681,6 +682,8 @@ def run_startup(
print("Detecting hardware...", flush=True) print("Detecting hardware...", flush=True)
hw = detect_hardware() hw = detect_hardware()
if force_cpu:
hw = with_forced_cpu(hw)
torch_thread_config = _configure_torch_threads(torch_threads, torch_interop_threads) torch_thread_config = _configure_torch_threads(torch_threads, torch_interop_threads)
if torch_thread_config: if torch_thread_config:
hw.update(torch_thread_config) hw.update(torch_thread_config)
@@ -697,6 +700,16 @@ def run_startup(
vram_mb = vram_mb_override vram_mb = vram_mb_override
shared_vram_mb = 0 shared_vram_mb = 0
print(f" Memory budget overridden to {vram_mb / 1024:.1f} GB via --memory", flush=True) print(f" Memory budget overridden to {vram_mb / 1024:.1f} GB via --memory", flush=True)
elif force_cpu:
if gpu_name and vram_mb > 0:
print(
f" --cpu: ignoring {gpu_name} "
f"({vram_mb / 1024:.1f} GB VRAM); running in CPU mode "
f"({ram_mb / 1024:.1f} GB RAM)",
flush=True,
)
else:
print(f" --cpu: running in CPU mode ({ram_mb / 1024:.1f} GB RAM)", flush=True)
elif device == "cpu": elif device == "cpu":
gpu_suffix = "" gpu_suffix = ""
if gpu_name and vram_mb > 0: if gpu_name and vram_mb > 0:
@@ -718,7 +731,7 @@ def run_startup(
print(f" Memory budget: {memory_budget_mb / 1024:.1f} GB {memory_budget_source}", flush=True) print(f" Memory budget: {memory_budget_mb / 1024:.1f} GB {memory_budget_source}", flush=True)
print("Benchmarking compute...", flush=True) print("Benchmarking compute...", flush=True)
if device != "cuda" and gpu_name: if device != "cuda" and gpu_name and not force_cpu:
_cuda_score, cuda_ok, cuda_error = benchmark_throughput_checked("cuda") _cuda_score, cuda_ok, cuda_error = benchmark_throughput_checked("cuda")
hw["cuda_benchmark_ok"] = cuda_ok hw["cuda_benchmark_ok"] = cuda_ok
if cuda_error: if cuda_error:
@@ -833,6 +846,7 @@ def run_startup(
cache_dir=cache_dir, cache_dir=cache_dir,
debug=debug, debug=debug,
max_loaded_shards=max_loaded_shards, max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
) )
_node_start_time = time.monotonic() _node_start_time = time.monotonic()
actual_port = node.start() actual_port = node.start()
@@ -986,6 +1000,7 @@ def run_startup(
cache_dir=cache_dir, cache_dir=cache_dir,
debug=debug, debug=debug,
max_loaded_shards=max_loaded_shards, max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
) )
_node_start_time = time.monotonic() _node_start_time = time.monotonic()
actual_port = node.start() actual_port = node.start()
@@ -1163,6 +1178,7 @@ def run_startup(
cache_dir=shard_path, cache_dir=shard_path,
debug=debug, debug=debug,
max_loaded_shards=max_loaded_shards, max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
) )
actual_port = node.start() actual_port = node.start()
total_layers = getattr(getattr(node, "backend", None), "total_layers", None) or assigned_total_layers total_layers = getattr(getattr(node, "backend", None), "total_layers", None) or assigned_total_layers

View File

@@ -1087,6 +1087,7 @@ class TorchNodeServer:
cache_dir: Path | None = None, cache_dir: Path | None = None,
debug: bool = False, debug: bool = False,
max_loaded_shards: int = 1, max_loaded_shards: int = 1,
force_cpu: bool = False,
) -> None: ) -> None:
self._host = host self._host = host
self._requested_port = port self._requested_port = port
@@ -1097,6 +1098,7 @@ class TorchNodeServer:
shard_end, shard_end,
quantization, quantization,
cache_dir, cache_dir,
force_cpu=force_cpu,
) )
self._backends: dict[str, TorchModelShard] = {self._backend.model_id: self._backend} self._backends: dict[str, TorchModelShard] = {self._backend.model_id: self._backend}
# Auto-detect tracker mode: enabled when shard_start == 0 or explicitly set # Auto-detect tracker mode: enabled when shard_start == 0 or explicitly set
@@ -1245,12 +1247,15 @@ def _load_backend(
shard_end: int, shard_end: int,
quantization: str, quantization: str,
cache_dir: Path | None = None, cache_dir: Path | None = None,
force_cpu: bool = False,
) -> TorchModelShard: ) -> TorchModelShard:
from .model_backend import load_torch_shard from .model_backend import load_torch_shard
quant = validate_quantization(quantization) quant = validate_quantization(quantization)
try: try:
return load_torch_shard(model_id, shard_start, shard_end, quant, cache_dir) return load_torch_shard(
model_id, shard_start, shard_end, quant, cache_dir, force_cpu=force_cpu
)
except MissingModelDependencyError: except MissingModelDependencyError:
raise raise
except InsufficientVRAMError as exc: except InsufficientVRAMError as exc:

View File

@@ -610,3 +610,44 @@ def test_default_cli_passes_advertise_host(monkeypatch):
assert captured["tracker_url"] == "http://192.168.0.179:8081" assert captured["tracker_url"] == "http://192.168.0.179:8081"
assert captured["advertise_host"] == "192.168.0.42" assert captured["advertise_host"] == "192.168.0.42"
assert captured["debug"] is True assert captured["debug"] is True
def test_default_cli_passes_force_cpu(monkeypatch):
"""`meshnet-node --cpu` forwards force_cpu into run_startup."""
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()
saved = {
"tracker_url": "http://localhost:8080",
"model_name": "stub-model",
"model_hf_repo": "",
"quantization": "auto",
"download_dir": "/tmp/models",
"wallet_path": "/tmp/wallet.json",
"port": 7000,
"host": "0.0.0.0",
}
monkeypatch.setattr(sys, "argv", ["meshnet-node", "--cpu"])
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["force_cpu"] is True

View File

@@ -34,6 +34,27 @@ from meshnet_tracker.server import TrackerServer
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_with_forced_cpu_overrides_device_but_keeps_gpu_inventory():
"""--cpu should register and run on CPU while preserving detected GPU metadata."""
import meshnet_node.hardware as hardware_mod
hw = hardware_mod.with_forced_cpu(
{
"device": "cuda",
"gpu_name": "NVIDIA GeForce RTX 4060",
"vram_mb": 8192,
"dedicated_vram_mb": 8192,
"shared_vram_mb": 0,
"ram_mb": 32768,
"cuda_available": True,
}
)
assert hw["device"] == "cpu"
assert hw["cuda_available"] is False
assert hw["gpu_name"] == "NVIDIA GeForce RTX 4060"
assert hw["vram_mb"] == 8192
def test_detect_hardware_returns_valid_profile(): def test_detect_hardware_returns_valid_profile():
"""Hardware detection always returns a dict with required keys.""" """Hardware detection always returns a dict with required keys."""
hw = detect_hardware() hw = detect_hardware()