Skip to content

feat: add native Intel XPU (torch.xpu) device support - #9401

Merged
lstein merged 41 commits into
invoke-ai:mainfrom
LexiconCode:feat/intel-xpu-support
Aug 15, 2026
Merged

feat: add native Intel XPU (torch.xpu) device support#9401
lstein merged 41 commits into
invoke-ai:mainfrom
LexiconCode:feat/intel-xpu-support

Conversation

@LexiconCode

@LexiconCode LexiconCode commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Native Intel XPU (torch.xpu) support for Arc / Battlemage GPUs.

Disclaimer: AI was used. However you can expect mutual respect for InvokeAI and person to person communication. I would appreciate feedback.

I've tried to keep this pr minimal. Most of the changes are for the new xpu back-end or memory probe — CUDA (incl. ROCm), MPS, and CPU paths are unchanged.

  • xpu / xpu:N device selection, fp16 default
  • VRAM detection via torch.xpu.mem_get_info(), with a fallback for setups missing the SYCL free-memory aspect (e.g. GPU passthrough VMs)
  • FP8 layerwise casting enabled via a cached runtime probe (fp8 storage + upcast; no fp8 matmul needed)
  • VAE auto-tiling, partial loading, OOM handling, and VRAM stats on XPU
  • [xpu] torch 2.13.0+
  • idle-GPU offload fix
  • Mock-based XPU tests mirroring the CUDA/MPS suites

Known limitations:

  • On the tested stack, VRAM exhaustion overcommits into host RAM and hangs rather than raising torch.OutOfMemoryError; the preflight mem_get_info budgeting is the operative defense.
  • BitsAndBytes NF4/INT8 both work on XPU (bitsandbytes supports Arc officially; verified on the B70 — NF4 needs the Level-Zero dev headers for its SYCL JIT). InvokeAI's own bnb wrappers still hardcode .cuda(), so enabling them is a small follow-up; until then GGUF and fp8 are the in-app quantized paths. However I'm unsure of the downstream effects of upgrading bitsandbytes on other back-ends.
  • Intel Arc Alchemist (A-series) has no native FP64. Battle Mage has FP64 hardware acceleration
  • Would benefit from testing on Windows and other Intel graphics cards besides B70 pro.

Related branches: fix/fp8-dequant-bf16 (fp8 checkpoint load-RAM fix), fix/flux-diffusers-vae (FLUX VAE classification fix).

Related Issues / Discussions

QA Instructions

Verified end to end on an Intel Arc Pro B70 (torch 2.7.1+xpu, Linux):

  • txt2img across SD1.5, SDXL, FLUX.1 (fp8 + GGUF), FLUX.2 (fp8 + GGUF), SD3.5, Z-Image, and CogView4
  • Wan 2.2 video generation (TI2V-5B), including the XPU VAE-tiling path on an oversized decode
  • fp8 layerwise casting verified on FLUX.1-dev (transformer resident at 11.3GB vs 22.7GB bf16)
  • Benchmarks land 1.3–1.7× behind an RTX 4090 on the same models/settings

To reproduce on Arc hardware: pip install invokeai[xpu], leave device: auto, generate. On any machine: pytest tests/backend/util/test_devices.py (44 tests, no GPU required).

Merge Plan

No special care needed. uv.lock is re-resolved for the [xpu] extra (additive); openapi.json/schema.ts regenerated for the device field values.

WHL file for Windows or Linux

invokeai-6.14.0a0-py3-none-any.zip Updated 8/2/2026

uv venv --python 3.12 invoke
invoke\Scripts\activate
uv pip install "invokeai-6.14.0a0-py3-none-any.whl[xpu]" --extra-index-url https://download.pytorch.org/whl/xpu --index-strategy unsafe-best-match
invokeai-web

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration (n/a — no slice changes)
  • Documentation added / updated (if applicable) (happy to add an Intel install docs section if desired)
  • Updated What's New copy (if doing a release after this PR)

@github-actions github-actions Bot added python PRs that change python files Root invocations PRs that change invocations backend PRs that change backend files services PRs that change app services frontend PRs that change frontend files python-tests PRs that change python tests python-deps PRs that change python dependencies labels Jul 29, 2026
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from 4f2d1e7 to c15e2b6 Compare July 30, 2026 01:23
@github-actions github-actions Bot added the api label Jul 30, 2026
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch 2 times, most recently from e3c67cf to 348c87c Compare July 30, 2026 01:38
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from 348c87c to 20bb0d8 Compare July 30, 2026 03:00
@lstein lstein self-assigned this Jul 31, 2026
@lstein lstein added the 6.14.1 label Jul 31, 2026
@lstein lstein moved this to 6.14.1: Bug fixes to 6.14.0 in Invoke - Community Roadmap Jul 31, 2026
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from 20bb0d8 to 409bd1a Compare July 31, 2026 22:11

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — it's a genuinely well-scoped contribution. I ran an adversarial review (the goal being to break it rather than to evaluate it), and I want to lead with what held up, because a lot did:

  • No CUDA / MPS / CPU regression found. Every refactor I traced is behaviour-preserving on the existing backends: the wan_latents_to_video restructure into total_vram: int | None is identical for CUDA, CPU, MPS and cpu_only VAEs; torch.cuda.OutOfMemoryErrortorch.OutOfMemoryError is a no-op (they're the same object); the model_cache.py device-label change still yields "cuda device" for an index-less CUDA device.
  • Packaging is clean. uv export --frozen --extra {cpu,cuda,rocm} is byte-identical before and after, except one added comment line. The 958-line uv.lock churn is entirely conflict-marker rewriting from adding xpu to the conflicts group — zero removals, zero version changes on shared packages. uv lock --check passes under both current uv and the CI-pinned 0.6.10.
  • The partial-loading machinery really is device-agnostic. I read cached_model_with_partial_load.py and all of torch_module_autocast/ looking for CUDA assumptions now reachable via running_with_dedicated_vram — no pinned memory, no streams, no .cuda(), no dtype assumptions. Your call to enable it on XPU is sound.
  • Your thread-locality comment is correct. I checked c10/xpu/XPUFunctions.cpp in v2.7.1: thread_local DeviceIndex curDeviceIndex. The multi-worker pinning is sound, and I couldn't construct an interleaving where normalize("xpu") resolves to another worker's GPU.
  • The test suite is thorough and passes locally (3859 passed, 119 skipped, 8 xfailed).

Requesting changes for the CI failures and a handful of real defects below.


1. CI is red on four checks (all mechanical)

invokeai/app/services/invocation_stats/invocation_stats_default.py:98 — the new ternary is ~158 chars; uv tool run ruff@0.11.2 format --check . rewrites it. This is the python-checks failure.

invokeai/app/services/events/events_common.py:143 — the source still reads "...(set only when running on a CUDA GPU)", but the committed openapi.json:45725 and schema.ts:18420 read "...on a GPU". It looks like the generated artifacts picked up an edit that didn't land in the Python. That single line is the entire openapi-checks and typegen-checks failure — fix the description in events_common.py and regenerate.

docs/src/generated/settings.json — not regenerated after the device / generation_devices description changes in config_default.py. This is the check-and-build failure (pnpm run generate-docs-data).


2. The XPU mem_get_info fallback feeds a formula that assumes different semantics

TorchDevice.xpu_mem_get_info falls back to total_memory - torch.xpu.memory_reserved(device). But memory_reserved() counts only this process's caching-allocator blocks, whereas torch.cuda.mem_get_info's free is a driver-global query. ModelCache._get_vram_available then applies the CUDA formula to both:

vram_available_to_process = vram_free + vram_allocated

which is only sound when vram_free is driver-global. The fallback is therefore high by (other processes + the Level-Zero context + compiled kernel binaries) — on Intel that's routinely 0.5–1.5 GB, and the Arc is usually also the display GPU.

Concrete trigger, using the exact scenario the docstring cites as the fallback's reason to exist — Arc 16 GiB in a passthrough VM, ~2 GiB held by the compositor + L0 context, fresh process, default device_working_mem_gb = 3:

  1. Fallback reports free = 16 GiBvram_available_to_process = 16 GiB
  2. _load_locked_model sees 13 GiB available and fully loads a 12 GiB transformer
  3. Real free is now ~2 GiB against a 3 GiB working-memory contract → OOM on the first denoise step
  4. The cache still believes it has room, so _offload_unlocked_models frees nothing on retry

The docstring's "budget hint rather than a guarantee" is fair, but the caller treats it as a hard budget. The same root cause hits attention.py:28 (auto_detect_slice_size picks "balanced" where it should pick "max").

Sub-case that's worse: if get_device_properties also fails, the function returns (0, 0). Work the arithmetic through _get_vram_available:

vram_available_to_process = 0 + allocated
vram_total_available_to_cache = allocated - working_mem
vram_cur_available_to_cache  = allocated - working_mem - _get_vram_in_use()   # == allocated
                             = -working_mem      # -3 GiB, forever, regardless of device state

Every subsequent lock() then evicts all unlocked models, unloads the model being locked, and calls partial_load_to_vram(-3221225472) — so everything runs permanently with per-layer CPU→GPU autocast and no error is surfaced. Meanwhile _calc_ram_available_to_model_cache takes the total or None branch and sizes a healthy-looking 32 GB RAM cache, so the logs look fine. Note this doesn't need a probe failure: max(total - reserved, 0) produces the same lock-in whenever reserved > total.

Minor, same function: the except (RuntimeError, AttributeError) doesn't catch what _lazy_init actually raises —

>>> TorchDevice.xpu_mem_get_info(torch.device('xpu', 0))
AssertionError: Torch not compiled with XPU enabled

Same for get_device_name at devices.py:158. Reaching it needs an XPU execution device on a non-XPU torch, which get_generation_devices rejects at startup — but the clause doesn't match its own documented failure mode. (Also: torch.xpu.mem_get_info does exist in 2.7.1, so the AttributeError arm is dead weight rather than a bug.)


3. The FP8 probe runs on the wrong device during idle-GPU offload

Three things compound here:

  1. _device_supports_fp8_storage(...) is the first statement in _should_use_fp8, before every exclusion — so it fires on the very first model load of any kind (tokenizer, VAE, anything).
  2. torch.zeros(2, device="xpu") is index-less, so it resolves through the thread's current XPU device.
  3. _maybe_offload_to_idle_gpu re-pins only InvokeAI's thread-local via TorchDevice.set_session_device(borrowed_device) — it never calls torch.xpu.set_device, unlike worker startup at session_processor_default.py:631-636, which does both.

So with generation_devices: [xpu:0, xpu:1] and offload_text_encoders_to_idle_gpus on (the default):

  1. Worker A is pinned to xpu:0 (torch.xpu.set_device(xpu:0) on that thread)
  2. A hits a CompelInvocation (idle_gpu_offloadable=True) and borrows xpu:1
  3. The ModelLoader's _torch_device is xpu:1
  4. The probe allocates on xpu:0 — the busy denoise GPU the offload existed to protect — and xpu:1's answer is inferred from xpu:0

load_model is also reachable from API/install threads, where the probe forces XPU lazy SYCL init on the FastAPI thread.

Two related issues in the same helper:

  • @lru_cache is keyed on the device type, not the device. Your own docstring says float8 support is build/driver dependent and "emerging" on Xe2 — that's a per-device property. On a discrete Arc + Iris Xe iGPU, whichever device probes first decides for both; on the wrong one, _apply_fp8_to_nn_module casts every Linear/Conv weight and the pre-hook upcast raises at forward time.
  • A transient failure is memoized forever, silently. except Exception: return False with maxsize=None, no logging, and cache_clear() is never called outside tests. The probe fires during a model load — i.e. exactly when the device may be full — so torch.OutOfMemoryError (a RuntimeError subclass), a UR/L0 transient, or "Cannot re-initialize XPU in forked subprocess" all permanently disable FP8 for the process. The only symptom is 2× VRAM and thrashing; the only cure is a restart. At minimum: log the exception, and don't cache failures.

Lastly, the probe doesn't exercise the runtime path. It tests float32 → float8_e4m3fn → float16 on XPU, but at runtime the storage cast happens on CPU (params are still CPU-resident at load time), and the XPU-side operations are the fp8 host→device copy plus the pre-hook's fp8 → bf16 upcast (compute_dtype is typically bf16 for Krea-2/FLUX). A build where fp8→fp16 works but fp8→bf16 doesn't would probe True and then fail at forward.


4. try_borrow lost its device-type filter

device_pool.py:99 checks only exclude.type not in _OFFLOAD_DEVICE_TYPES, then takes any registered key != exclude_key. Before this PR only CUDA devices could ever be registered, so cross-type borrowing was structurally impossible; now both types share one pool.

generation_devices: ["cuda:0", "xpu:0"] is accepted by both validators (config_default.py:285, app_info.py:123) and by TorchDevice.get_generation_devices, so a cuda:0 session would be handed xpu:0 for its text encoder. Reachability is low (the pinned +cu128 / +xpu wheels are mutually exclusive, so this needs a custom build), but the fix is one line — filter candidates by key.startswith(exclude.type).


5. Design question: Intel integrated GPUs

Unlike CUDA, where every enumerated device is a discrete accelerator, Level Zero enumerates the CPU's iGPU alongside any discrete card (and on Data Center GPU Max, each tile separately unless ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE). Two consequences:

  • devices.py:220-221_all_available_devices takes range(torch.xpu.device_count()) with no filter, so on the mainstream Arc configuration (iGPU + discrete Arc) generation_devices: auto resolves to [xpu:0, xpu:1] and dispatches half the queue to the iGPU. _OFFLOAD_DEVICE_TYPES also makes it a borrow target, so try_borrow may hand it an 8.9 GB text encoder.
  • model_cache.py:599running_with_dedicated_vram = type in ("cuda", "xpu") treats an iGPU as having dedicated VRAM. The comment three lines above states the policy MPS is excluded for — "memory is shared with the CPU" — which is exactly an iGPU's topology, yet it takes the elif "xpu" branch at :1103 rather than the MPS branch at :1107 that deliberately uses psutil.virtual_memory().available.

On, say, a 16 GB Lunar Lake laptop: get_device_properties(igpu).total_memory reports a large share of system RAM, so heuristic 2 sizes the RAM cache from it and _get_vram_available budgets the same DRAM again — doubled once more by keep_ram_copy_of_weights=True. Heuristic 2's stated intent ("cap the RAM cache at 1× VRAM") is meaningless when "VRAM" is the RAM being capped.

I don't think torch exposes a clean is_integrated flag (I checked XPUDeviceProp.h — there's architecture and gpu_eu_count, but nothing direct), so this may be a name/arch heuristic or simply a documented ZE_AFFINITY_MASK note. Happy to hear what you think is right; I'd rather not guess for you.


6. Smaller items

  • pins.json has no xpu entry, so the launcher will have no XPU install option — users would be stuck with the manual pip install invokeai[xpu] from your description. Worth adding alongside the extra.
  • vae_working_memory.py:207 (untouched by this PR) is the only estimator that branches on backend, and it branches is_rocm = torch.version.hip is not None — so XPU silently gets the CUDA constants (2900/1600), which assume flash/efficient attention. If XPU's SDPA falls back to math attention the way ROCm's does, the right constants are ~2–4× larger. Given your note that XPU exhaustion hangs rather than raising, this is an unpleasant one to guess wrong on, and Qwen isn't in your QA list. Worth a spot-check on Qwen Image / Qwen Image Edit.
  • invocation_stats_default.py:31, memory_snapshot.py:50, model_cache.py:1333 all dispatch on torch.cuda.is_available() first. On a mixed NVIDIA + Arc box running on xpu:0, node stats and the summary report 0.0 GB while the Arc is full, and the cache log prints "CUDA Memory Allocated: 0.0 MB". Diagnostic-only, but it'd make XPU bug reports hard to act on. Unlike _get_vram_in_use, none of these consult self._execution_device.
  • anima_latents_to_image.py:59"out_of_host_memory" is Intel's catch-all for driver-side resource failures (kernel compilation, handle exhaustion), not just host OOM, so a genuinely broken decode gets a pointless tiled retry. Bounded and re-raises, so cosmetic. (The lowercasing itself is correct — I checked every needle.)
  • Metadata only: buildSD1Graph.ts:144 / buildSDXLGraph.ts:159 write rand_device: 'cuda' into image metadata on XPU machines.
  • Docs: nothing under docs/ mentions xpu or the [xpu] extra. You offered an Intel install section — yes please.

Nothing here is architectural; the shape of the change is right. §1 is mechanical, §2 and §3 are the ones I'd want fixed before merge, and §5 is a genuine question rather than a demand. Thanks again for the care that went into this, and for the detailed QA notes — they made the review much easier.

@fishd72

fishd72 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Tried this on Fedora 44 on my Framework 13 Pro which has the new PantherLake X7 385H with Intel Arc B390 integrated graphics, 32GB system RAM and it ran just fine. Performance seems better than my Apple MacBook Pro 14 with M2 Max chip (30GPU cores).

Full install requires:

  1. Download the zip file above, extract to a directory
  2. Install Intel compute runtimes with sudo dnf install intel-compute-runtime
  3. Create a directory for invoke (I use ~/invokeai), change into this directory
  4. Create a venv folder using uv venv --python 3.12 .venv
  5. Activate venv using source .venv/bin/activate
  6. Install Invoke using uv pip <path to extracted file>/invokeai-6.14.0a0-py3-none-any.whl[xpu] --extra-index-url https://download.pytorch.org/whl/xpu --index-strategy unsafe-best-match
  7. On my device, to increase performance I used the latest Torch versions by removing the prior versions: uv pip uninstall torch torchvision pytorch-triton-xpu, then installed the versions from the nightly repo: uv pip install torch torchvision --index-url https://download.pytorch.org/whl/nightly/xpu/
  8. Launch invoke from the command line using .venv/bin/invokeai-web

Only tested SDXL so far but will try other models shortly.

@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from d2d1c40 to 8cdca1a Compare August 2, 2026 13:16
@github-actions github-actions Bot added the docs PRs that change docs label Aug 2, 2026
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from 8cdca1a to 4c6363e Compare August 2, 2026 13:18
@LexiconCode

Copy link
Copy Markdown
Contributor Author

@lstein I've tried my best to address your concerns including igpu.

§1 All four CI failures fixed — the 151-char line, events_common.py wording, and regenerated settings.json.

§2 Fixed, and measured. On torch 2.7.1 mem_get_info() genuinely raises on my Arc Pro B70 — so the blind estimate was the only source, exactly as you described. Added a Level Zero Sysman tier between native and the estimate; it returned 29.04/31.72 GiB where native failed. With 16 GiB held by another process the estimate over-reports by 16.17 GiB while Sysman is correct. (0, 0) now raises rather than collapsing the budget, and the except is broad since the type moves between torch releases. attention.py:28 fixes with it.

§3 Probe moved below the exclusions, targets the given device, caches per-device, no longer memoises failures, and now mirrors the runtime path (CPU cast → host→device copy → bf16/fp16 upcast).

§4 Borrows restricted to the excluded device's own type.

§5 Level Zero does expose it — ZE_DEVICE_PROPERTY_FLAG_INTEGRATED, read via ctypes against the loader that already ships with torch+xpu. iGPUs are dropped from auto when a discrete GPU exists, and no longer counted as dedicated VRAM. Unknown answers and iGPU-only machines keep current behaviour. I don't have the hardware to test this!

§6 All done. On the VAE constants — I measured XPU SDPA scaling instead of guessing: peak memory doubles as sequence length doubles (2.00× across 2048→16384), so XPU is in CUDA's O(area) regime and those constants are right.

Also bumped the extra to torch 2.13.0+xpu. Measured on the same hardware, identical code, only torch differing: SD1.5 −9%, SDXL −10%, Z-Image −16%, Wan video −33%. cpu/cuda/rocm exports are unchanged package-for-package.

One open question. @fishd72 reports an Arc B390 iGPU below. My §5 change switches partial loading off there, and makes heuristic 2's cap inert. I've asked for numbers before assuming that's an improvement. Related: _calc_ram_available_to_model_cache sizes from virtual_memory().total while _get_vram_available uses .available — on shared memory those are the same pool. Worth a separate look.

@fishd72 Thanks integrated-GPU report! Four things if you have a moment:

  1. With a model loaded, what does free -g show in the available column?
  2. What number follows Calculated model RAM cache size in the startup log?
  3. Tried anything bigger than SDXL — FLUX.1 or Qwen Image?
  4. Could you run the snippet below? It reports whether Level Zero flags the B390 as integrated, which is what the iGPU handling keys off.
import ctypes, ctypes.util, struct, psutil, torch
lib = ctypes.CDLL(ctypes.util.find_library("ze_loader") or "libze_loader.so.1")
u32p, vpp = ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)
for fn, args in (("zeInit",[ctypes.c_uint32]), ("zeDriverGet",[u32p,vpp]),
                 ("zeDeviceGet",[ctypes.c_void_p,u32p,vpp]),
                 ("zeDeviceGetProperties",[ctypes.c_void_p,ctypes.c_void_p])):
    f = getattr(lib, fn); f.argtypes, f.restype = args, ctypes.c_int
assert lib.zeInit(0) == 0, "zeInit failed"
def enum(fn, parent=None):
    n = ctypes.c_uint32(0); head = [parent] if parent is not None else []
    getattr(lib, fn)(*head, ctypes.byref(n), None)
    arr = (ctypes.c_void_p * n.value)()
    getattr(lib, fn)(*head, ctypes.byref(n), arr)
    return list(arr[:n.value])
for i, d in enumerate(dv for drv in enum("zeDriverGet") for dv in enum("zeDeviceGet", drv)):
    buf = ctypes.create_string_buffer(368); struct.pack_into("I", buf, 0, 3)
    lib.zeDeviceGetProperties(d, buf)
    name = buf.raw[112:368].split(b"\0")[0].decode(errors="replace")
    print(f"device {i}: integrated={bool(struct.unpack_from('I', buf, 28)[0] & 1)}  name={name!r}")
vm = psutil.virtual_memory()
print(f"torch {torch.__version__}  xpu devices {torch.xpu.device_count()}")
for i in range(torch.xpu.device_count()):
    p = torch.xpu.get_device_properties(torch.device("xpu", i))
    print(f"  xpu:{i} total_memory {p.total_memory/2**30:.1f} GB")
print(f"RAM total {vm.total/2**30:.1f} GB  available {vm.available/2**30:.1f} GB")

Read-only — it just queries the driver.

Gives the launcher an Intel install option instead of requiring a manual
pip install of the extra.
All three sites dispatched on torch.cuda.is_available() first, so a mixed
NVIDIA + Arc box running on xpu reported a constant 0.0 GB and logged
"CUDA Memory Allocated" -- which would make XPU bug reports unactionable.
… OOM needle


XPU SDPA was measured on Arc Pro B70 / torch 2.13+xpu: peak memory doubles when
sequence length doubles (2.00x across 2048-16384; 2.0 MB at seq=16384 vs 512 MB
for a materialised score matrix). So XPU is in CUDA's O(area) regime, not ROCm's
math-attention regime, and the existing constants are correct rather than
accidental.
Was hardcoded to 'cuda' for any non-CPU noise, which is wrong on Arc. Falls
back to 'cuda' when the device query has not resolved, so Nvidia metadata is
unchanged.
The probe was the first statement in _should_use_fp8, so it allocated on the GPU
during the first load of any model at all -- tokenizer, VAE, scheduler -- and on
API/install threads it forced XPU lazy SYCL init on a thread that never generates.
Moved below the exclusions.
The blind estimate (total minus this process's reserved bytes) is what made
_get_vram_available over-commit on a shared GPU: it feeds a formula that assumes
a driver-global figure. Sysman's zesMemoryGetState reports that figure and is
often available when the SYCL ext_intel_free_memory aspect is not, so try it
before estimating.

Measured on Arc Pro B70 with 16 GiB held by another process: Sysman reported
15.553 GiB free, the estimate 31.725 -- a 16.172 GiB error, exactly the foreign
allocation. Sysman is not a guaranteed substitute (torch's query bottoms out in
the same layer), so the estimate remains as a last resort.
The storage cast happens on CPU while params are still CPU-resident, then the
fp8 tensor is copied to the device and the pre-hook upcasts there. Probing all
three steps on the device would pass on a build where the host->device fp8 copy
or one upcast target fails, and break at forward time instead.

Verified on Arc Pro B70 / torch 2.13+xpu: the full sequence works on both cards.
torch.xpu.get_device_name goes through _lazy_init, which raises AssertionError
on a build without XPU. Naming is used only for labels and logs, so fall back to
the device string rather than propagating.
Returning None for a device with no index would skip the driver-global query and
fall through to the blind estimate with no visible symptom. Callers currently
always pass a concrete device, so this is a latent hazard rather than a live bug.
Handles come back from (c_void_p * n)() as plain Python ints, and ctypes
converts an undeclared int argument to a C int -- 32 bits. Any handle above
2**31 was being silently truncated; a direct test of that path segfaults.
It happened to work on the B70 because the handles fit.

Also: release the idle-GPU borrow if re-pinning raises (the setup was outside
the try, so a failure there stranded the lock for the life of the process), and
report a failing fp8 probe once per device instead of on every model load.
Setting a process-wide environment variable from a read-only query leaks into
child processes. It also bought nothing: the variable only gates Sysman on
runtimes predating zesInit and must be set before Level Zero initialises, which
torch has already done by then. Verified on Arc Pro B70 that zesInit succeeds
with the variable unset.
level_zero: cache the loader so it is opened and its prototypes configured once
rather than twice, share the driver/device enumeration and its ordering guard
between the two probes, and collapse the Sysman pair of globals into one
nullable tuple.

Also: fp8 support cache is a set (it only ever stored True), the pbr_maps
empty_cache is routed through TorchDevice like the PR's other conversions, the
shared-memory VRAM branch stops re-testing the device type it matched on,
`_auto_generation_devices` partitions in one pass, and rand_device only answers
when every generation device is the same accelerator.

Merges three duplicate mem_get_info tests into one parametrized case and drops
two fp8 probe tests fully subsumed by the cast-sequence test.
Intel's XPU backend matured considerably after 2.7.1: torch.xpu.mem_get_info()
works on driver/kernel combinations where it previously raised, and the oneAPI
user-space runtime ships with the wheel, so upgrading torch upgrades it too.
Follows the rocm extra, which already pins ahead of cpu/cuda.

pytorch-triton-xpu was renamed triton-xpu upstream. The darwin/aarch64
fallbacks stay on 2.7.1 to match the other extras and the project's
torch<2.8.0 constraint on darwin.

cpu/cuda/rocm exports are unchanged package-for-package (196/211/197); the only
delta is a dropped "via pytorch-triton-xpu" comment annotation from the rename.
Without it the test only passes where Level Zero cannot answer -- never on the
Intel hardware the probe exists for, where Sysman returns before the tier under
test is reached.
Its per-platform allowlist rejects anything unlisted, so pins.json's xpu entry
fails it. PyTorch publishes XPU wheels for win32 and linux x86_64, matching the
extra's markers.
torch.xpu.set_device() brings up a SYCL context that holds VRAM in an otherwise
idle process, the same reason the CUDA pin waits for the first claimed queue item.
Regenerating on Windows flips two path defaults to backslashes, which the docs
check rejects.
Their VRAM is system RAM, so a RAM copy doubles each model's footprint against
the same pool. Drop it, letting a full load move weights rather than copy them.

Keep partial loading on -- it is the only path that respects vram_available --
and raise a clean error when a full-load-only model cannot fit, instead of
walking into an uncatchable OOM-kill. Warn once when a setting is overridden.

Scoped to integrated XPU; CPU and MPS are unchanged.
`auto` prefers CUDA on a mixed Nvidia/Arc box, and keep_ram_copy_of_weights is
ignored on an integrated GPU.
@LexiconCode
LexiconCode force-pushed the feat/intel-xpu-support branch from 2208577 to c5b83b6 Compare August 13, 2026 03:47
@LexiconCode

Copy link
Copy Markdown
Contributor Author

I am not able to reproduce header too large. I'm hoping to get this merged relatively soon

lstein and others added 3 commits August 15, 2026 12:48
Resolves the openapi.json conflict by regenerating it from the merged tree
(scripts/generate_openapi_schema.py + prettier). schema.ts regenerated
identically to the auto-merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ll to move

A resident model's weights occupy the same DRAM that vram_available is read
from, so its total can exceed "available" precisely because it is loaded.
lock() runs on every use and full_load_to_vram() is a no-op when resident;
comparing the total refused the re-lock and evicted a healthy model on every
other generation. Compare what full_load_to_vram() will actually move instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ModelCache.__init__ sizes the RAM cache from the device's total VRAM, which on
an xpu execution device reads torch.xpu.get_device_properties() -- an
AssertionError on the CPU-only builds CI runs, failing 9 of these tests before
they reached their subject. Stub a fixed total during construction, and add a
regression test for the resident-model re-lock guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed adversarially at c5b83b673e plus the three commits I pushed (details below). Approving. Every round-2 item is genuinely fixed, and fishd72's confirmation that keep_ram_copy_of_weights: false resolves the Z-Image Q8 crash validates the double-occupancy diagnosis empirically — the fix in 77f2d0705a bakes exactly that in, correctly scoped.

What I verified

  • The rebase is faithful. I interdiffed all 31 rebased commits against the previously-reviewed series: every delta is a legitimate conflict weave with main (HiDiffusion params, the fp8 util refactor, the legacy-device wording, the uv.lock re-lock). One deliberate change: the app_info total-VRAM stat now uses torch.xpu.get_device_properties().total_memory instead of the Sysman tier — right call for a "total" figure, and it avoids the context-creating call.
  • keep_ram_copy forced off on integrated XPU via _is_integrated_xpu(), with CPU/MPS deliberately untouched — and it composes correctly with the shared CPU-weights store from #9403: the store only engages when a CPU state dict exists, so keep_ram_copy=False bypasses it cleanly in both cached-model classes. Steady-state footprint drops from 2× to 1×, as intended.
  • Partial loading restored on integrated XPU — the one bounded loading path is back, and the warn-once notice covers the overridden setting.
  • settings.json regenerated on Linux — verified drift-free against a local regeneration.
  • Sysman stub in the unknown-total probe test, pins-check xpu index entry, and the deferred torch.xpu.set_device pin (mirroring the CUDA #9413 rationale, with a sound reason for bypassing _set_torch_current_device's availability guard) all check out.

What I pushed (maintainer edits, as offered)

  1. 961c443545 — merge of main. Only openapi.json conflicted; I resolved it by regenerating from the merged tree (generate_openapi_schema.py + prettier). The regenerated schema.ts came out byte-identical to git's auto-merge, and the resolved openapi.json differs from main by exactly the xpu description/pattern changes — two independent consistency checks on the resolution.

  2. 0645f906dd — a logic fix in the new full-load guard. The pre-check compared the model's total bytes against vram_available, but on unified memory a resident model's own weights are missing from free system memory, and full_load_to_vram() is a no-op when resident. Concrete failure: a 14 GB Q8 model on a 31 GB iGPU with enable_partial_loading: false — generation 1 loads and works; generation 2 re-locks the resident model, sees ~10 GB "available", raises the new OOM error, and the except handler evicts the healthy model; generation 3 reloads from scratch and works; generation 4 fails again. The guard now compares the bytes still to be moved (zero for a resident model), which is also what full_load_to_vram() will actually allocate. Regression test included; I verified it fails against the pre-fix code.

  3. d04d443cc5 — made test_model_cache_integrated_gpu.py runnable on CPU-only torch, which is what CI runs. 9 of the 13 tests failed before reaching their subject: _make_cache("xpu:0") constructed the ModelCache outside the integrated-GPU stub, so the constructor's RAM-cache heuristic treated the device as dedicated and called torch.xpu.get_device_propertiesAssertionError: Torch not compiled with XPU enabled. This never surfaced because the PR has been in a conflicted state since the tests were written, so no CI ever ran on them. Construction now stubs a fixed VRAM total.

Local verification on the final tree: the integrated-GPU file passes 15/15 on CPU torch; the full model-manager + util + config test dirs pass (988 passed); ruff check + format clean at CI's pinned 0.11.2. I also ran an independent adversarial pass over my own two fixes before pushing (guard lifecycle, cur_vram_bytes desync states, the last-resort unload interleaving, stub scoping) — no defects found.

Non-blocking note for a possible follow-up

The full-load guard is conservative for fresh loads in one narrow config. With keep_ram_copy now forced off on integrated XPU, a full load is a per-tensor move from CPU RAM into the same DRAM — net-zero footprint, transient peak of about one tensor. But the model's weights already occupy CPU RAM at lock time, so they're already subtracted from the free-memory figure the guard reads: a model larger than what's left will be refused even though the move would have succeeded. Only reachable with enable_partial_loading: false (default is true, and partial loading sidesteps the guard entirely), and the failure is a clean, actionable error rather than a crash — so I'm fine shipping it as is. If you want to refine it later: gate the check on a CPU copy actually being retained (get_cpu_state_dict() is not None), which is when a load genuinely allocates a second footprint.

@fishd72 — thanks for testing. With this branch you can drop the keep_ram_copy_of_weights: false workaround; it's now automatic on your B390. The Anima "header too large" safetensors error is a different animal — that error means the file's on-disk header is malformed, which usually indicates a truncated or corrupted download; re-downloading the file is the first thing to try.

The merge conflict that kept CI dark for two rounds is resolved, and all checks are now green — python tests, python checks, frontend checks and tests, typegen, openapi, docs, uv lock, and lfs — the first full CI pass in this PR's life. Nice work landing all of this — the memory-topology handling on integrated GPUs is now in better shape than the MPS path it was modeled on.

@lstein
lstein merged commit f2af8c6 into invoke-ai:main Aug 15, 2026
17 checks passed
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Aug 17, 2026
Brings in the three commits this branch was behind: Intel XPU device support
(invoke-ai#9401), single-file Wan 2.2 checkpoints (invoke-ai#9503) and the opt-in Wan low-VRAM
mode (invoke-ai#9462).

Only uv.lock conflicted. This branch lifts fastapi to 0.141.1, which adds
annotated-doc and typing-inspection as dependencies, while invoke-ai#9401 rewrote every
marker string in the file to carry the new xpu extra. Regenerated with 'uv lock'
instead of hand-merging those marker chains; the result has both (fastapi
0.141.1, 776 xpu markers).

openapi.json and schema.ts merged without conflict, and were checked rather than
assumed: both are supersets of main (143 -> 145 paths, 870 -> 877 schemas,
187 -> 189 operations), so nothing from main was dropped.
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Aug 17, 2026
Resolves a conflict in `_should_use_fp8` where both sides restructured the
same guard chain.

Upstream moved device support probing to the end of the chain (invoke-ai#9401, XPU),
so it runs only for a model that actually wants FP8. This branch still had
the older `_torch_device.type != "cuda"` precondition at the top, which would
have short-circuited every non-CUDA device and undone that. Dropped it; the
trailing `_device_supports_fp8_storage` call covers the same ground.

Also reconciles a semantic conflict git merged cleanly: upstream added a
parametrised case pinning Z-Image as excluded, while this stack removes that
exclusion (invoke-ai#9414 gives Z-Image checkpoints a uniform model dtype, and
`test_should_use_fp8_allows_z_image` documents why the exclusion is obsolete).
Replaced the Z-Image case with a quantized one, which pins the property this
branch is actually about: the quantized-format guard runs ahead of the device
probe. The now-unused `BaseModelType` import is gone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.1 api backend PRs that change backend files docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-deps PRs that change python dependencies python-tests PRs that change python tests Root services PRs that change app services

Projects

Status: 6.14.1: Bug fixes to 6.14.0

Development

Successfully merging this pull request may close these issues.

3 participants