This commit is contained in:
liminfei-amd 2026-08-15 22:27:25 +02:00 committed by GitHub
commit 6f08c34836
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 342 additions and 0 deletions

View File

@ -433,6 +433,98 @@ def is_amd():
return True
return False
def is_integrated_gpu(device=None):
# AMD APUs / integrated GPUs expose host RAM (GTT/shared) as device memory
# via mem_get_info(); torch flags these as integrated. See ComfyUI #14274.
if cpu_state != CPUState.GPU:
return False
if not (is_nvidia() or is_amd()):
return False
try:
if device is None:
device = get_torch_device()
return bool(getattr(torch.cuda.get_device_properties(device), "is_integrated", 0))
except Exception:
return False
def _amd_vram_gtt_totals(device=None):
# Best-effort (vram_total, gtt_total) in bytes from the amdgpu sysfs nodes
# mem_info_vram_total / mem_info_gtt_total, or None when they cannot be read
# (e.g. NVIDIA Tegra integrated parts that have no dedicated VRAM). #14274
if not is_amd():
return None
try:
drm_root = "/sys/class/drm"
candidates = []
for name in os.listdir(drm_root):
if not (name.startswith("card") and name[len("card"):].isdigit()):
continue
dev_dir = os.path.join(drm_root, name, "device")
vram_path = os.path.join(dev_dir, "mem_info_vram_total")
gtt_path = os.path.join(dev_dir, "mem_info_gtt_total")
if not (os.path.exists(vram_path) and os.path.exists(gtt_path)):
continue
try:
with open(os.path.join(dev_dir, "vendor")) as vf:
if vf.read().strip().lower() != "0x1002":
continue
except OSError:
pass
candidates.append((os.path.basename(os.path.realpath(dev_dir)), vram_path, gtt_path))
if not candidates:
return None
chosen = None
target_bdf = None
try:
if device is None:
device = get_torch_device()
props = torch.cuda.get_device_properties(device)
# torch reports the PCI location as integers (pci_domain_id / pci_bus_id
# / pci_device_id); amdgpu names its sysfs nodes as a hex
# "domain:bus:device.function" BDF. Build the canonical hex BDF so the
# two are comparable (the old str(pci_bus_id) compared a decimal bus
# number against a hex BDF string and could never match). #14274
target_bdf = "%04x:%02x:%02x" % (
int(getattr(props, "pci_domain_id", 0) or 0),
int(getattr(props, "pci_bus_id", 0) or 0),
int(getattr(props, "pci_device_id", 0) or 0),
)
except Exception:
target_bdf = None
if target_bdf:
for pci, vram_path, gtt_path in candidates:
# candidates carry the realpath() leaf BDF (domain:bus:device.function),
# so matching the domain:bus:device part works whether the GPU is
# attached directly or sits behind a PCIe bridge (nested sysfs path). #14274
if pci.lower().rsplit(".", 1)[0] == target_bdf:
chosen = (vram_path, gtt_path)
break
if chosen is None and len(candidates) == 1:
chosen = (candidates[0][1], candidates[0][2])
if chosen is None:
return None
with open(chosen[0]) as f:
vram_total = int(f.read().strip())
with open(chosen[1]) as f:
gtt_total = int(f.read().strip())
return (vram_total, gtt_total)
except Exception:
return None
def integrated_gpu_is_shared_heavy(device=None):
# For an integrated GPU, decide whether its memory is dominated by the shared
# GTT/host-RAM aperture (treat as UMA -> SHARED) or by a large dedicated VRAM
# carveout (keep NORMAL/HIGH_VRAM). Keys on the amdgpu mem_info_vram_total vs
# mem_info_gtt_total ratio (ComfyUI #14274). Defaults to True when the totals
# are unavailable (e.g. NVIDIA Tegra parts that have no dedicated VRAM).
totals = _amd_vram_gtt_totals(device)
if totals is None:
return True
vram_total, gtt_total = totals
if not vram_total or vram_total <= 0:
return True
return gtt_total >= vram_total
def amd_min_version(device=None, min_rdna_version=0):
if not is_amd():
return False
@ -578,6 +670,15 @@ if cpu_state != CPUState.GPU:
if cpu_state == CPUState.MPS:
vram_state = VRAMState.SHARED
if vram_state == VRAMState.NORMAL_VRAM and is_integrated_gpu() and integrated_gpu_is_shared_heavy():
# Integrated/UMA GPU whose shared GTT/host-RAM pool dominates the (small)
# dedicated VRAM carveout: treat as UMA and use SHARED so the shared pool is
# not double-counted as dedicated VRAM (#14274). Dedicated-heavy integrated
# parts (large BIOS UMA carveout, e.g. Strix Halo) keep NORMAL_VRAM where
# HIGH_VRAM is correct.
vram_state = VRAMState.SHARED
logging.info("Integrated GPU with shared-memory-dominant pool detected (UMA): using SHARED vram state to avoid double-counting GTT/shared memory as dedicated VRAM.")
logging.info(f"Set vram state to: {vram_state.name}")
DISABLE_SMART_MEMORY = args.disable_smart_memory

View File

@ -0,0 +1,241 @@
import builtins
import io
from types import SimpleNamespace
import pytest
import torch
from comfy.cli_args import args as cli_args
original_cpu_arg = cli_args.cpu
if not torch.cuda.is_available():
cli_args.cpu = True
import comfy.model_management as model_management # noqa: E402
cli_args.cpu = original_cpu_arg
MIXED_TOPOLOGY = {
"card1": {
"realpath": "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/0000:02:00.0/0000:03:00.0",
"vram": 17163091968,
"gtt": 16359088128,
},
"card2": {
"realpath": "/sys/devices/pci0000:00/0000:00:01.2/0000:04:00.0/0000:05:00.0/0000:06:00.0",
"vram": 17163091968,
"gtt": 16359088128,
},
"card3": {
"realpath": "/sys/devices/pci0000:00/0000:00:08.1/0000:1d:00.0",
"vram": 536870912,
"gtt": 16359088128,
},
}
def install_fake_drm(monkeypatch, cards):
files = {}
realpaths = {}
for card, values in cards.items():
device_dir = model_management.os.path.join("/sys/class/drm", card, "device")
files[model_management.os.path.join(device_dir, "vendor")] = values.get("vendor", "0x1002")
files[model_management.os.path.join(device_dir, "mem_info_vram_total")] = str(values["vram"])
files[model_management.os.path.join(device_dir, "mem_info_gtt_total")] = str(values["gtt"])
realpaths[device_dir] = values["realpath"]
monkeypatch.setattr(
model_management.os,
"listdir",
lambda path: [*cards, "card1-DP-1"] if path == "/sys/class/drm" else [],
)
monkeypatch.setattr(model_management.os.path, "exists", lambda path: path in files)
monkeypatch.setattr(model_management.os.path, "realpath", lambda path: realpaths.get(path, path))
monkeypatch.setattr(
builtins,
"open",
lambda path, *args, **kwargs: io.StringIO(files[str(path)]),
)
def properties(bus, *, integrated, domain=0, device=0):
return SimpleNamespace(
pci_domain_id=domain,
pci_bus_id=bus,
pci_device_id=device,
is_integrated=int(integrated),
)
@pytest.fixture(autouse=True)
def gpu_mode(monkeypatch):
monkeypatch.setattr(model_management, "cpu_state", model_management.CPUState.GPU)
monkeypatch.setattr(model_management, "is_amd", lambda: True)
monkeypatch.setattr(model_management, "is_nvidia", lambda: False)
def test_mixed_topology_maps_each_device_by_canonical_bdf(monkeypatch):
install_fake_drm(monkeypatch, MIXED_TOPOLOGY)
device_properties = {
0: properties(3, integrated=False),
1: properties(6, integrated=False),
2: properties(29, integrated=True),
}
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: device_properties[device.index],
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) == (
17163091968,
16359088128,
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 1)) == (
17163091968,
16359088128,
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 2)) == (
536870912,
16359088128,
)
assert model_management.is_integrated_gpu(torch.device("cuda", 0)) is False
assert model_management.is_integrated_gpu(torch.device("cuda", 1)) is False
assert model_management.is_integrated_gpu(torch.device("cuda", 2)) is True
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 0)) is False
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 1)) is False
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 2)) is True
def test_dedicated_heavy_integrated_gpu_stays_non_shared(monkeypatch):
install_fake_drm(monkeypatch, {"card1": MIXED_TOPOLOGY["card1"]})
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(3, integrated=True),
)
device = torch.device("cuda", 0)
assert model_management.is_integrated_gpu(device) is True
assert model_management.integrated_gpu_is_shared_heavy(device) is False
@pytest.mark.parametrize(
("vram", "gtt", "expected"),
[
(1024, 1023, False),
(1024, 1024, True),
(1024, 1025, True),
(0, 1024, True),
],
)
def test_shared_heavy_threshold(monkeypatch, vram, gtt, expected):
card = {**MIXED_TOPOLOGY["card3"], "vram": vram, "gtt": gtt}
install_fake_drm(monkeypatch, {"card3": card})
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(29, integrated=True),
)
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 0)) is expected
def test_full_domain_bus_device_bdf_is_matched(monkeypatch):
card = {
**MIXED_TOPOLOGY["card3"],
"realpath": "/sys/devices/pci1234:00/1234:ab:1f.0",
}
install_fake_drm(monkeypatch, {"card3": card})
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(0xAB, integrated=True, domain=0x1234, device=0x1F),
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) == (
536870912,
16359088128,
)
def test_multiple_candidates_without_bdf_match_do_not_fall_back(monkeypatch):
install_fake_drm(monkeypatch, MIXED_TOPOLOGY)
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(127, integrated=True),
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) is None
def test_non_amd_drm_candidate_is_ignored(monkeypatch):
cards = {
"card1": MIXED_TOPOLOGY["card1"],
"card2": {
**MIXED_TOPOLOGY["card2"],
"vendor": "0x10de",
"vram": 1,
"gtt": 2,
},
}
install_fake_drm(monkeypatch, cards)
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(6, integrated=False),
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) == (
17163091968,
16359088128,
)
def test_single_candidate_can_fall_back_when_pci_properties_are_unavailable(monkeypatch):
install_fake_drm(monkeypatch, {"card3": MIXED_TOPOLOGY["card3"]})
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: (_ for _ in ()).throw(RuntimeError("properties unavailable")),
)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) == (
536870912,
16359088128,
)
def test_unavailable_totals_default_integrated_gpu_to_shared(monkeypatch):
install_fake_drm(monkeypatch, {})
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) is None
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 0)) is True
def test_non_integer_totals_default_integrated_gpu_to_shared(monkeypatch):
card = {**MIXED_TOPOLOGY["card3"], "vram": "invalid", "gtt": 16359088128}
install_fake_drm(monkeypatch, {"card3": card})
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) is None
assert model_management.integrated_gpu_is_shared_heavy(torch.device("cuda", 0)) is True
def test_cpu_state_never_reports_integrated_gpu(monkeypatch):
monkeypatch.setattr(model_management, "cpu_state", model_management.CPUState.CPU)
monkeypatch.setattr(
model_management.torch.cuda,
"get_device_properties",
lambda device: properties(29, integrated=True),
)
assert model_management.is_integrated_gpu(torch.device("cuda", 0)) is False
def test_non_amd_backend_has_no_amd_totals(monkeypatch):
install_fake_drm(monkeypatch, MIXED_TOPOLOGY)
monkeypatch.setattr(model_management, "is_amd", lambda: False)
assert model_management._amd_vram_gtt_totals(torch.device("cuda", 0)) is None