-cpu flag
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user