Compare commits
3 Commits
08826f6ace
...
23b15ed0ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b15ed0ae | ||
|
|
2f5936c8ed | ||
|
|
1d3d3018cd |
@@ -132,7 +132,8 @@ Install **one** torch line into the same env as `meshnet-node`:
|
|||||||
|----------|---------|
|
|----------|---------|
|
||||||
| NVIDIA CUDA | `pip install torch` (default index) |
|
| NVIDIA CUDA | `pip install torch` (default index) |
|
||||||
| CPU only | `pip install torch --index-url https://download.pytorch.org/whl/cpu` |
|
| CPU only | `pip install torch --index-url https://download.pytorch.org/whl/cpu` |
|
||||||
| AMD ROCm | `pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.3` |
|
| AMD ROCm (discrete, arch in official wheels) | `pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.3` |
|
||||||
|
| AMD Strix Halo / Ryzen AI Max (gfx1151) | `pip install torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx1151/` |
|
||||||
|
|
||||||
On Windows `.venv`, prefix with `.\.venv\Scripts\pip.exe`. Conda users with CUDA
|
On Windows `.venv`, prefix with `.\.venv\Scripts\pip.exe`. Conda users with CUDA
|
||||||
torch already installed can skip this step.
|
torch already installed can skip this step.
|
||||||
@@ -182,6 +183,20 @@ python -m pip install "transformers>=5.12" accelerate safetensors
|
|||||||
python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.3
|
python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Strix Halo / Ryzen AI Max APUs (gfx1151, e.g. Radeon 8060S):** the official
|
||||||
|
`download.pytorch.org` ROCm wheels do NOT ship gfx1151 kernels — every GPU op
|
||||||
|
fails with `HIP error: invalid device function` and `HSA_OVERRIDE_GFX_VERSION`
|
||||||
|
does not help. Install AMD's gfx1151-native builds instead (TheRock nightlies,
|
||||||
|
self-contained, no system ROCm required):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/v2/gfx1151/
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that your arch is actually in the wheel:
|
||||||
|
`python -c "import torch; print(torch.cuda.get_arch_list())"` must list your
|
||||||
|
GPU's `gcnArchName` (from `torch.cuda.get_device_properties(0)`).
|
||||||
|
|
||||||
Keep this separate from a known-good CPU `.venv` until ROCm is verified on that
|
Keep this separate from a known-good CPU `.venv` until ROCm is verified on that
|
||||||
machine. ROCm wheels are large and host-runtime-sensitive; a failed ROCm install
|
machine. ROCm wheels are large and host-runtime-sensitive; a failed ROCm install
|
||||||
should not break the CPU fallback environment.
|
should not break the CPU fallback environment.
|
||||||
@@ -241,6 +256,11 @@ HF_HOME=/path/to/models .venv-rocm/bin/meshnet-node start \
|
|||||||
or `libroctx64.so` are host ROCm runtime problems, not meshnet-node problems.
|
or `libroctx64.so` are host ROCm runtime problems, not meshnet-node problems.
|
||||||
- Some AMD APUs and consumer GPUs require newer ROCm/Radeon support than server
|
- Some AMD APUs and consumer GPUs require newer ROCm/Radeon support than server
|
||||||
Instinct cards. Check AMD's ROCm Radeon/Ryzen support matrix for the exact model.
|
Instinct cards. Check AMD's ROCm Radeon/Ryzen support matrix for the exact model.
|
||||||
|
- `HIP error: invalid device function` (or `no kernel image is available`) on a
|
||||||
|
working driver means the installed torch wheel has no kernels compiled for
|
||||||
|
your GPU arch. Compare `torch.cuda.get_device_properties(0).gcnArchName`
|
||||||
|
against `torch.cuda.get_arch_list()`; if your arch is missing, install a wheel
|
||||||
|
built for it (see the Strix Halo/gfx1151 note above).
|
||||||
|
|
||||||
### Qwen3.5/3.6-MoE notes
|
### Qwen3.5/3.6-MoE notes
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,24 @@ def _detect_nvidia_smi_gpu_memory() -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_torch_cuda_inventory(torch_module) -> dict | None:
|
||||||
|
"""Return torch-visible CUDA/HIP GPU metadata without running kernels."""
|
||||||
|
try:
|
||||||
|
if not torch_module.cuda.is_available() or torch_module.cuda.device_count() < 1:
|
||||||
|
return None
|
||||||
|
idx = torch_module.cuda.current_device()
|
||||||
|
name = torch_module.cuda.get_device_name(idx)
|
||||||
|
props = torch_module.cuda.get_device_properties(idx)
|
||||||
|
vram_mb = int(props.total_memory // (1024 * 1024))
|
||||||
|
gpu = {"gpu_name": name, "vram_mb": max(0, vram_mb)}
|
||||||
|
gcn_arch = getattr(props, "gcnArchName", None)
|
||||||
|
if gcn_arch:
|
||||||
|
gpu["gcn_arch"] = str(gcn_arch)
|
||||||
|
return gpu
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _torch_cuda_is_executable(torch_module) -> bool:
|
def _torch_cuda_is_executable(torch_module) -> bool:
|
||||||
"""Return True only if this Python process can execute a CUDA tensor op."""
|
"""Return True only if this Python process can execute a CUDA tensor op."""
|
||||||
try:
|
try:
|
||||||
@@ -139,7 +157,7 @@ def _torch_cuda_is_executable(torch_module) -> bool:
|
|||||||
def _gpu_inventory_profile(gpu: dict | None, ram_mb: int) -> dict | None:
|
def _gpu_inventory_profile(gpu: dict | None, ram_mb: int) -> dict | None:
|
||||||
if gpu is None:
|
if gpu is None:
|
||||||
return None
|
return None
|
||||||
return {
|
profile = {
|
||||||
"device": "cpu",
|
"device": "cpu",
|
||||||
"gpu_name": gpu["gpu_name"],
|
"gpu_name": gpu["gpu_name"],
|
||||||
"vram_mb": gpu["vram_mb"],
|
"vram_mb": gpu["vram_mb"],
|
||||||
@@ -148,20 +166,26 @@ def _gpu_inventory_profile(gpu: dict | None, ram_mb: int) -> dict | None:
|
|||||||
"ram_mb": ram_mb,
|
"ram_mb": ram_mb,
|
||||||
"cuda_available": False,
|
"cuda_available": False,
|
||||||
}
|
}
|
||||||
|
if gpu.get("gcn_arch"):
|
||||||
|
profile["gcn_arch"] = gpu["gcn_arch"]
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
torch_gpu: dict | None = None
|
||||||
try:
|
try:
|
||||||
import torch # type: ignore[import]
|
import torch # type: ignore[import]
|
||||||
|
|
||||||
|
torch_gpu = _detect_torch_cuda_inventory(torch)
|
||||||
if _torch_cuda_is_executable(torch):
|
if _torch_cuda_is_executable(torch):
|
||||||
idx = torch.cuda.current_device()
|
if torch_gpu is None:
|
||||||
name = torch.cuda.get_device_name(idx)
|
torch_gpu = _detect_torch_cuda_inventory(torch)
|
||||||
props = torch.cuda.get_device_properties(idx)
|
name = torch_gpu["gpu_name"] if torch_gpu is not None else "CUDA GPU"
|
||||||
vram_mb = props.total_memory // (1024 * 1024)
|
vram_mb = torch_gpu["vram_mb"] if torch_gpu is not None else 0
|
||||||
shared_vram_mb = max(0, ram_mb // 2)
|
shared_vram_mb = max(0, ram_mb // 2)
|
||||||
return {
|
profile = {
|
||||||
"device": "cuda",
|
"device": "cuda",
|
||||||
"gpu_name": name,
|
"gpu_name": name,
|
||||||
"vram_mb": vram_mb,
|
"vram_mb": vram_mb,
|
||||||
@@ -170,9 +194,16 @@ def detect_hardware() -> dict:
|
|||||||
"ram_mb": ram_mb,
|
"ram_mb": ram_mb,
|
||||||
"cuda_available": True,
|
"cuda_available": True,
|
||||||
}
|
}
|
||||||
|
if torch_gpu is not None and torch_gpu.get("gcn_arch"):
|
||||||
|
profile["gcn_arch"] = torch_gpu["gcn_arch"]
|
||||||
|
return profile
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
torch_inventory = _gpu_inventory_profile(torch_gpu, ram_mb)
|
||||||
|
if torch_inventory is not None:
|
||||||
|
return torch_inventory
|
||||||
|
|
||||||
nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb)
|
nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb)
|
||||||
if nvidia_gpu is not None:
|
if nvidia_gpu is not None:
|
||||||
return nvidia_gpu
|
return nvidia_gpu
|
||||||
|
|||||||
@@ -39,6 +39,26 @@ class KVCacheMiss(ModelBackendError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _torch_cuda_is_executable(torch_module: Any) -> bool:
|
||||||
|
"""Return True only when this process can actually execute a CUDA/HIP op.
|
||||||
|
|
||||||
|
On ROCm, ``torch.cuda.is_available()`` can be true for an AMD GPU even when
|
||||||
|
the installed PyTorch wheel has no runnable kernels for that GPU target.
|
||||||
|
Loading weights onto such a device can segfault in native code, so the model
|
||||||
|
backend must use the same executable-device check as startup hardware
|
||||||
|
detection rather than trusting inventory alone.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not torch_module.cuda.is_available():
|
||||||
|
return False
|
||||||
|
probe = torch_module.empty((1,), device="cuda")
|
||||||
|
probe += 1
|
||||||
|
torch_module.cuda.synchronize()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TensorPayload:
|
class TensorPayload:
|
||||||
body: bytes
|
body: bytes
|
||||||
@@ -209,7 +229,7 @@ class TorchModelShard:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
self.torch = torch
|
self.torch = torch
|
||||||
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
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,
|
||||||
|
|||||||
@@ -128,6 +128,59 @@ def test_nvidia_smi_without_torch_cuda_keeps_cpu_execution(monkeypatch):
|
|||||||
assert hw["ram_mb"] == 80 * 1024
|
assert hw["ram_mb"] == 80 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_rocm_inventory_is_reported_when_kernels_are_not_executable(monkeypatch):
|
||||||
|
"""ROCm can expose GPU metadata even when this torch wheel cannot run kernels."""
|
||||||
|
import meshnet_node.hardware as hardware_mod
|
||||||
|
|
||||||
|
class FakeProps:
|
||||||
|
total_memory = 64 * 1024 * 1024 * 1024
|
||||||
|
gcnArchName = "gfx1151"
|
||||||
|
|
||||||
|
class FakeCuda:
|
||||||
|
@staticmethod
|
||||||
|
def is_available():
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def device_count():
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def current_device():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_device_name(_idx):
|
||||||
|
return "AMD Radeon 8060S"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_device_properties(_idx):
|
||||||
|
return FakeProps()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def synchronize():
|
||||||
|
raise AssertionError("synchronize should not run after empty() fails")
|
||||||
|
|
||||||
|
fake_torch = types.SimpleNamespace(
|
||||||
|
cuda=FakeCuda(),
|
||||||
|
empty=lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||||
|
RuntimeError("HIP error: invalid device function")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(hardware_mod, "_detect_ram_mb", lambda: 125 * 1024)
|
||||||
|
monkeypatch.setitem(sys.modules, "torch", fake_torch)
|
||||||
|
|
||||||
|
hw = hardware_mod.detect_hardware()
|
||||||
|
|
||||||
|
assert hw["device"] == "cpu"
|
||||||
|
assert hw["cuda_available"] is False
|
||||||
|
assert hw["gpu_name"] == "AMD Radeon 8060S"
|
||||||
|
assert hw["vram_mb"] == 64 * 1024
|
||||||
|
assert hw["shared_vram_mb"] == 64_000
|
||||||
|
assert hw["gcn_arch"] == "gfx1151"
|
||||||
|
|
||||||
|
|
||||||
def test_memory_budget_uses_ram_for_cpu_and_shared_memory_for_cuda():
|
def test_memory_budget_uses_ram_for_cpu_and_shared_memory_for_cuda():
|
||||||
assert _memory_budget("cpu", vram_mb=8192, ram_mb=80 * 1024, shared_vram_mb=40 * 1024) == (
|
assert _memory_budget("cpu", vram_mb=8192, ram_mb=80 * 1024, shared_vram_mb=40 * 1024) == (
|
||||||
80 * 1024,
|
80 * 1024,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from meshnet_node.model_backend import (
|
|||||||
_should_partial_materialize_shard,
|
_should_partial_materialize_shard,
|
||||||
_decoder_attention_mask,
|
_decoder_attention_mask,
|
||||||
_int_tensor_header,
|
_int_tensor_header,
|
||||||
|
_torch_cuda_is_executable,
|
||||||
build_quantization_config,
|
build_quantization_config,
|
||||||
validate_quantization,
|
validate_quantization,
|
||||||
)
|
)
|
||||||
@@ -209,6 +210,26 @@ def test_bitsandbytes_configs_are_created_lazily(monkeypatch):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_rocm_inventory_without_executable_kernels_is_not_used_as_cuda():
|
||||||
|
class FakeCuda:
|
||||||
|
@staticmethod
|
||||||
|
def is_available():
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def synchronize():
|
||||||
|
raise AssertionError("synchronize should not run after empty() fails")
|
||||||
|
|
||||||
|
fake_torch = types.SimpleNamespace(
|
||||||
|
cuda=FakeCuda(),
|
||||||
|
empty=lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||||
|
RuntimeError("HIP error: invalid device function")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _torch_cuda_is_executable(fake_torch) is False
|
||||||
|
|
||||||
|
|
||||||
def test_head_forward_accepts_text_prompt_and_returns_bfloat16_activations():
|
def test_head_forward_accepts_text_prompt_and_returns_bfloat16_activations():
|
||||||
node = TorchNodeServer(backend=_FakeBackend())
|
node = TorchNodeServer(backend=_FakeBackend())
|
||||||
port = node.start()
|
port = node.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user