-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 |
|------|---------|
| *(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 |
| `--gpu` | Install and verify a GPU runtime; **fail hard** if GPU execution cannot be confirmed after install |
| `--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 (installer only — not implemented on `meshnet-node` yet) |
| `--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
hardware detection in `meshnet_node.hardware.detect_hardware()` — that profile still
feeds tracker registration and routing.
Installer flags set install-time intent. At runtime, `meshnet-node` auto-uses GPU when
CUDA works; pass `--cpu` to ignore it. Hardware metadata (GPU name/VRAM) is still
detected for diagnostics.
### v1 install pipeline
@@ -243,7 +243,7 @@ Not in Ralph yet. See
| 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 |
| 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 |
| 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 |

View File

@@ -69,6 +69,7 @@ def _run_node(cfg: dict) -> None:
torch_threads=cfg.get("torch_threads"),
torch_interop_threads=cfg.get("torch_interop_threads"),
node_name=cfg.get("node_name"),
force_cpu=bool(cfg.get("force_cpu", False)),
)
except Exception as exc:
print(f"\nERROR: {exc}", file=sys.stderr, flush=True)
@@ -174,6 +175,8 @@ def _cmd_default(args) -> int:
overrides["torch_threads"] = args.torch_threads
if getattr(args, "torch_interop_threads", None) is not None:
overrides["torch_interop_threads"] = args.torch_interop_threads
if getattr(args, "cpu", False):
overrides["force_cpu"] = True
if overrides:
cfg = merge_cli_overrides(cfg, **overrides)
@@ -276,6 +279,7 @@ def _cmd_start(args) -> int:
torch_threads=getattr(args, "torch_threads", None),
torch_interop_threads=getattr(args, "torch_interop_threads", None),
node_name=cfg.get("node_name"),
force_cpu=getattr(args, "cpu", False),
)
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr, flush=True)
@@ -332,6 +336,8 @@ def main() -> None:
help="Set PyTorch intra-op CPU worker threads")
parser.add_argument("--torch-interop-threads", type=int, metavar="N",
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("--no-tui", action="store_true", help="Plain-text output (no rich dashboard)")
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")
start_cmd.add_argument("--torch-interop-threads", type=int, metavar="N",
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("--tracker-source-disabled", action="store_true",
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
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:
"""Detect GPU model and available VRAM. Returns hardware profile dict."""
ram_mb = _detect_ram_mb()

View File

@@ -212,6 +212,7 @@ class TorchModelShard:
shard_end: int,
quantization: Quantization = "auto",
cache_dir: Path | None = None,
force_cpu: bool = False,
) -> None:
if shard_start < 0 or shard_end < 0 or shard_start > shard_end:
raise ValueError("shard_start must be <= shard_end and non-negative")
@@ -229,7 +230,10 @@ class TorchModelShard:
) from exc
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
quant_config, dtype, uses_quantized_weights = _model_load_plan(
AutoConfig,
@@ -682,8 +686,11 @@ def load_torch_shard(
shard_end: int,
quantization: Quantization = "auto",
cache_dir: Path | None = None,
force_cpu: bool = False,
) -> 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:

View File

@@ -15,7 +15,7 @@ from pathlib import Path
from typing import Any
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 .relay_bridge import RelayHttpBridge, peer_id_from_wallet
from .server import StubNodeServer
@@ -640,6 +640,7 @@ def run_startup(
torch_threads: int | None = None,
torch_interop_threads: int | None = None,
node_name: str | None = None,
force_cpu: bool = False,
) -> StubNodeServer | TorchNodeServer:
"""Execute the full startup sequence and return a running node server.
@@ -681,6 +682,8 @@ def run_startup(
print("Detecting hardware...", flush=True)
hw = detect_hardware()
if force_cpu:
hw = with_forced_cpu(hw)
torch_thread_config = _configure_torch_threads(torch_threads, torch_interop_threads)
if torch_thread_config:
hw.update(torch_thread_config)
@@ -697,6 +700,16 @@ def run_startup(
vram_mb = vram_mb_override
shared_vram_mb = 0
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":
gpu_suffix = ""
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("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")
hw["cuda_benchmark_ok"] = cuda_ok
if cuda_error:
@@ -833,6 +846,7 @@ def run_startup(
cache_dir=cache_dir,
debug=debug,
max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
)
_node_start_time = time.monotonic()
actual_port = node.start()
@@ -986,6 +1000,7 @@ def run_startup(
cache_dir=cache_dir,
debug=debug,
max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
)
_node_start_time = time.monotonic()
actual_port = node.start()
@@ -1163,6 +1178,7 @@ def run_startup(
cache_dir=shard_path,
debug=debug,
max_loaded_shards=max_loaded_shards,
force_cpu=force_cpu,
)
actual_port = node.start()
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,
debug: bool = False,
max_loaded_shards: int = 1,
force_cpu: bool = False,
) -> None:
self._host = host
self._requested_port = port
@@ -1097,6 +1098,7 @@ class TorchNodeServer:
shard_end,
quantization,
cache_dir,
force_cpu=force_cpu,
)
self._backends: dict[str, TorchModelShard] = {self._backend.model_id: self._backend}
# Auto-detect tracker mode: enabled when shard_start == 0 or explicitly set
@@ -1245,12 +1247,15 @@ def _load_backend(
shard_end: int,
quantization: str,
cache_dir: Path | None = None,
force_cpu: bool = False,
) -> TorchModelShard:
from .model_backend import load_torch_shard
quant = validate_quantization(quantization)
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:
raise
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["advertise_host"] == "192.168.0.42"
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():
"""Hardware detection always returns a dict with required keys."""
hw = detect_hardware()