Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b759a03
Enable studio for Intel GPU (XPU / Level Zero)
danielhanchen Apr 11, 2026
a1c2c4b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 11, 2026
6c55664
Address review feedback for PR #4724: hybrid-host CVD preservation, F…
danielhanchen Apr 16, 2026
a293d25
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
b8b7d47
Round 2 review fixes: idle GPUs, hybrid hint opt-in, relative ordinal…
danielhanchen Apr 16, 2026
eebf077
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
3d579d9
Round 3 review fixes: FLAT gpu_ids contract, FORCE_XPU opt-in, wildca…
danielhanchen Apr 16, 2026
cc04baa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
90ffb22
Round 4 review fixes: replace silent excepts with debug logging
danielhanchen Apr 16, 2026
b3ace02
Round 5 review fixes: FLAT ID contract, hybrid telemetry, OOM matcher
danielhanchen Apr 16, 2026
24b2a30
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
4a0fc3d
Round 6 review fixes: XPU device_map and telemetry index_kind
danielhanchen Apr 16, 2026
3fa23d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
a57adb2
Round 7 review fixes: enable XPU FLAT auto-select and placement
danielhanchen Apr 16, 2026
95ac005
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
c1d9249
Round 8 review fixes: preserve inherited ZE_AFFINITY_MASK
danielhanchen Apr 16, 2026
3756b5c
Round 9 Gemini fix: skip empty tokens when parsing CUDA_VISIBLE_DEVICES
danielhanchen Apr 16, 2026
efef89a
Round 10 review fixes: revert ordinal synthesis, use HF balanced instead
danielhanchen Apr 16, 2026
f32a546
Trim verbose code comments across hardware.py and llama_cpp.py
danielhanchen Apr 16, 2026
b3844e0
Merge branch 'main' into zhenyuan_enable_studio
rolandtannous Apr 23, 2026
70e573d
Merge branch 'main' into zhenyuan_enable_studio
danielhanchen Jul 9, 2026
7d60f2e
Studio: add full Intel XPU spoof pipeline test and wire it into CI
danielhanchen Jul 9, 2026
d2e6d15
Studio: tighten Intel XPU comments in hardware and trainer
danielhanchen Jul 9, 2026
6d5e73f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
95b9e0d
Export get_torch_device_str via a wrapper so the import-hoist lint pa…
danielhanchen Jul 10, 2026
ffb08ba
Drop stale xpu-smi mention from the auto-select comment
danielhanchen Jul 10, 2026
6611253
Hide CUDA when UNSLOTH_FORCE_XPU wins on a hybrid host
danielhanchen Jul 10, 2026
79b56d0
Fix pre-detect GPU masking and extend VRAM coexistence guards to XPU
danielhanchen Jul 10, 2026
c216fd2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2026
3d87200
Harden the pre-detect XPU build probe in apply_gpu_ids
danielhanchen Jul 10, 2026
7db43d7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2026
a4a7140
Mirror detect_hardware's XPU hint in the pre-detect mask decision
danielhanchen Jul 10, 2026
6df063f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2026
202a1b4
Propagate the parent's detected backend to worker GPU masking
danielhanchen Jul 10, 2026
6439a34
Isolate the XPU probe in get_package_versions
danielhanchen Jul 10, 2026
117c58f
Studio: refine GGUF per-GPU selection (gpu_ids)
Jul 20, 2026
fd8eb76
Merge commit '3d379cdb81ea6b1688eee4812ff5a6e1e85c2c74' into r7239
danielhanchen Jul 21, 2026
7c0ca2a
Merge PR 7239 into current main
oobabooga Jul 23, 2026
0663dc5
Merge current main and PR 7239 into Intel XPU support
oobabooga Jul 23, 2026
99ad75d
Merge main into zhenyuan_enable_studio
oobabooga Jul 24, 2026
f537d65
Fix XPU visibility fallbacks
oobabooga Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion studio/backend/core/inference/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from transformers import TextStreamer
from peft import PeftModel, PeftModelForCausalLM

import contextlib
import json
import sys
import torch
Expand Down Expand Up @@ -1646,8 +1647,30 @@ def _generate_dac(
+ text
+ "<|text_end|>\n<|audio_start|><|global_features_start|>\n"
)

with torch.inference_mode():
with torch.amp.autocast("cuda", dtype = model.dtype):
# Derive the autocast device from the loaded model, not from the
# global backend: a CPU-fallback DAC on an XPU/CUDA host must not
# open a GPU autocast context around CPU tensors.
device_type = (
model.device.type
if hasattr(model.device, "type")
else str(model.device).split(":", 1)[0]
)
# Clamp to autocast-supported backends so exotic devices
# (e.g. "meta" during accelerate offloaded loading) do not raise.
# MPS is autocast-supported since torch 2.3, keep it in the set.
if device_type not in ("cuda", "xpu", "mps", "cpu"):
device_type = "cpu"
# CPU and XPU autocast only accept bfloat16/float16. For a
# float32 model, skip autocast entirely to avoid raising or
# producing a warning on every generate call.
autocast_dtype_supported = model.dtype in (torch.bfloat16, torch.float16)
if device_type in ("cpu", "xpu") and not autocast_dtype_supported:
autocast_ctx = contextlib.nullcontext()
else:
autocast_ctx = torch.amp.autocast(device_type, dtype = model.dtype)
Comment on lines +1964 to +1968

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Autocast is only designed for low-precision mixed precision (such as float16 or bfloat16). If the model's dtype is float32 (or any other non-low-precision dtype), running autocast is redundant and will raise a ValueError in PyTorch on other backends like CUDA or MPS. We can simplify this check to use contextlib.nullcontext() whenever autocast_dtype_supported is False, regardless of the device type.

Suggested change
autocast_dtype_supported = model.dtype in (torch.bfloat16, torch.float16)
if device_type in ("cpu", "xpu") and not autocast_dtype_supported:
autocast_ctx = contextlib.nullcontext()
else:
autocast_ctx = torch.amp.autocast(device_type, dtype = model.dtype)
autocast_dtype_supported = model.dtype in (torch.bfloat16, torch.float16)
if not autocast_dtype_supported:
autocast_ctx = contextlib.nullcontext()
else:
autocast_ctx = torch.amp.autocast(device_type, dtype = model.dtype)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fp32 skip is deliberately limited to cpu/xpu, where autocast raises on fp32. CUDA warns and disables autocast for fp32, which is exactly the behavior main has always shipped (the current code produces a context identical to main's hardcoded autocast('cuda', dtype=model.dtype) for every CUDA model). Extending the skip to CUDA/MPS would change NVIDIA behavior, which is out of scope for this Intel enablement PR.

with autocast_ctx:
inputs = tokenizer([prompt], return_tensors = "pt").to(model.device)
generated = model.generate(
**inputs,
Expand Down
143 changes: 111 additions & 32 deletions studio/backend/core/inference/llama_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

import httpx

from utils.hardware import clear_gpu_cache

logger = get_logger(__name__)

# ── Pre-compiled patterns for plan-without-action re-prompt ──
Expand Down Expand Up @@ -322,14 +324,19 @@ def _get_gguf_size_bytes(model_path: str) -> int:

@staticmethod
def _get_gpu_free_memory() -> list[tuple[int, int]]:
"""Query free memory per GPU via nvidia-smi.

Returns list of (gpu_index, free_mib) sorted by index.
Respects CUDA_VISIBLE_DEVICES if set.
Returns empty list if nvidia-smi is not available.
"""Query free memory per visible GPU, backend-aware.

Returns list of ``(gpu_index, free_mib)`` sorted by index. The index
space matches whatever the active backend exposes: physical
``nvidia-smi`` indices on NVIDIA; parent-visible numeric IDs on
AMD/ROCm and Intel XPU (via Studio's hardware telemetry layer).
Returns an empty list if no per-GPU free-memory data is available,
which lets the caller fall through to a non-placement launch path.
"""
import os

# Fast path: NVIDIA / nvidia-smi. Cheap, authoritative, and already
# battle-tested across the CUDA fleet. Keep it exactly as-is.
try:
result = subprocess.run(
[
Expand All @@ -341,30 +348,82 @@ def _get_gpu_free_memory() -> list[tuple[int, int]]:
text = True,
timeout = 10,
)
if result.returncode != 0:
if result.returncode == 0:
# Parse which GPUs are allowed by existing CUDA_VISIBLE_DEVICES
allowed = None
cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
if cvd is not None and cvd.strip():
try:
allowed = set(int(x.strip()) for x in cvd.split(","))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The list comprehension for parsing CUDA_VISIBLE_DEVICES should filter out empty strings to avoid a ValueError when calling int() on a trailing or double comma (e.g., "0,1,"). While the surrounding try...except block prevents a crash, it causes the entire filter to be ignored (allowed becomes None), which might lead to unexpected GPU selection. Filtering empty tokens ensures that numeric masks with trailing commas are still parsed correctly, consistent with the logic used in utils/hardware/hardware.py.

Suggested change
allowed = set(int(x.strip()) for x in cvd.split(","))
allowed = set(int(x.strip()) for x in cvd.split(",") if x.strip())
References
  1. Avoid using broad, silent exception handlers like except Exception: pass. Instead, log the exception, even if at a debug level, to aid in future debugging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dropped when llama_cpp.py was reset to main; main's _visible_devices_mask already filters empty tokens.

except ValueError:
pass # Non-numeric (e.g., "GPU-uuid"), ignore filter

gpus = []
for line in result.stdout.strip().splitlines():
parts = line.split(",")
if len(parts) == 2:
idx = int(parts[0].strip())
free_mib = int(parts[1].strip())
if allowed is not None and idx not in allowed:
continue
gpus.append((idx, free_mib))
if gpus:
return sorted(gpus, key = lambda item: item[0])
except FileNotFoundError:
pass # nvidia-smi not on PATH — fall through to generic path
except Exception as e:
logger.debug(f"nvidia-smi free-memory query failed: {e}")

# Generic path: AMD ROCm, Intel XPU, or any host where nvidia-smi is
# absent / returned empty. Uses Studio's backend-aware telemetry
# layer so XPU hosts pick up free memory via torch.xpu /
# mem_get_info (populated by utils/hardware/hardware.py), and
# ROCm hosts pick up AMD smi data.
try:
from utils.hardware import get_visible_gpu_utilization

utilization = get_visible_gpu_utilization()

# Refuse to return relative ordinals. When the backend exposes
# the device set via index_kind="relative" (subdevice / wildcard /
# UUID masks), those indices are NOT safe to round-trip back into
# ZE_AFFINITY_MASK or CUDA_VISIBLE_DEVICES because the parent
# process has already hidden the physical ID mapping. Returning
# [] lets the caller skip the placement path entirely and
# inherit the parent's visibility mask unchanged.
if utilization.get("index_kind") not in (None, "physical"):
logger.debug(
"Skipping GPU placement: telemetry reports index_kind=%r "
"(not physical)",
utilization.get("index_kind"),
)
return []

# Parse which GPUs are allowed by existing CUDA_VISIBLE_DEVICES
allowed = None
cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
if cvd is not None and cvd.strip():
try:
allowed = set(int(x.strip()) for x in cvd.split(","))
except ValueError:
pass # Non-numeric (e.g., "GPU-uuid"), ignore filter

gpus = []
for line in result.stdout.strip().splitlines():
parts = line.split(",")
if len(parts) == 2:
idx = int(parts[0].strip())
free_mib = int(parts[1].strip())
if allowed is not None and idx not in allowed:
continue
gpus.append((idx, free_mib))
return gpus
gpus: list[tuple[int, int]] = []
for device in utilization.get("devices", []) or []:
index = device.get("index")

# Use explicit ``is None`` checks -- ``or`` would treat an
# idle GPU with vram_used_gb == 0.0 as missing telemetry and
# silently drop a perfectly valid free card.
total_gb = device.get("vram_total_gb")
if total_gb is None:
total_gb = device.get("total_gb")

used_gb = device.get("vram_used_gb")
if used_gb is None:
used_gb = device.get("used_gb")

if index is None or total_gb is None or used_gb is None:
# Missing telemetry for this device -- skip rather than
# invent a free-memory number that drives placement.
continue

free_mib = max(int((float(total_gb) - float(used_gb)) * 1024), 0)
gpus.append((int(index), free_mib))
return sorted(gpus, key = lambda item: item[0])
except Exception as e:
logger.debug(f"Failed to query GPU free memory via nvidia-smi: {e}")
logger.debug(f"Generic GPU free-memory query failed: {e}")
return []

@staticmethod
Expand Down Expand Up @@ -1512,9 +1571,25 @@ def load_model(
f"{new_ld}:{existing_ld}" if existing_ld else new_ld
)

# Pin to selected GPU(s) via CUDA_VISIBLE_DEVICES
# Pin to selected GPU(s) via the backend-appropriate visibility
# env var: CUDA_VISIBLE_DEVICES on NVIDIA/ROCm, ZE_AFFINITY_MASK
# on Intel XPU (llama-server's SYCL build reads ZE_AFFINITY_MASK,
# not CUDA_VISIBLE_DEVICES).
if gpu_indices is not None:
env["CUDA_VISIBLE_DEVICES"] = ",".join(str(i) for i in gpu_indices)
from utils.hardware import get_device
from utils.hardware.hardware import DeviceType

mask = ",".join(str(i) for i in gpu_indices)
if get_device() == DeviceType.XPU:
env["ZE_AFFINITY_MASK"] = mask
# Deliberately preserve any inherited CUDA_VISIBLE_DEVICES
# (may be "" on hybrid NVIDIA+Intel hosts to force the
# child onto Intel/SYCL). Popping it here would let a
# SYCL+CUDA llama.cpp build re-discover NVIDIA devices,
# contradicting the design note in apply_gpu_ids() in
# utils/hardware/hardware.py.
else:
env["CUDA_VISIBLE_DEVICES"] = mask

self._stdout_lines = []
self._process = subprocess.Popen(
Expand Down Expand Up @@ -1625,10 +1700,7 @@ def unload_model(self) -> bool:
if LlamaCppBackend._codec_mgr is not None:
LlamaCppBackend._codec_mgr.unload()
LlamaCppBackend._codec_mgr = None
import torch

if torch.cuda.is_available():
torch.cuda.empty_cache()
clear_gpu_cache()
return True

def _kill_process(self):
Expand Down Expand Up @@ -3261,6 +3333,11 @@ def init_audio_codec(self, audio_type: str) -> None:
if LlamaCppBackend._codec_mgr is None:
LlamaCppBackend._codec_mgr = AudioCodecManager()

# Preserve the pre-PR CPU fallback on non-CUDA hosts: the SNAC /
# BiCodec / DAC codecs are not yet validated on Intel XPU, so
# only promote to a GPU device when CUDA is actually available.
# A follow-up can extend this once an XPU-specific codec path is
# added.
device = "cuda" if torch.cuda.is_available() else "cpu"
model_repo_path = None

Expand Down Expand Up @@ -3333,6 +3410,8 @@ def generate_audio_response(
else None
)

# Match init_audio_codec: stay on CPU for non-CUDA hosts until the
# codec path is validated on XPU.
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"
Expand Down
17 changes: 14 additions & 3 deletions studio/backend/core/training/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,10 @@ def _preprocess_snac_dataset(self, dataset, custom_format_mapping = None):

SNAC_MODEL_NAME = "hubertsiuzdak/snac_24khz"
SNAC_SAMPLE_RATE = 24000

# SNAC codec has not been validated on Intel XPU yet; keep the
# pre-PR CPU fallback for non-CUDA hosts until an XPU-specific
# path is added.
device = "cuda" if torch.cuda.is_available() else "cpu"
max_length = self.max_seq_length or 2048
tokenizer = self.tokenizer
Expand Down Expand Up @@ -1716,7 +1720,8 @@ def _preprocess_snac_dataset(self, dataset, custom_format_mapping = None):
import gc

gc.collect()
torch.cuda.empty_cache()

clear_gpu_cache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The function clear_gpu_cache is called here but is not imported in this file, which will cause a NameError at runtime. Please add from utils.hardware import clear_gpu_cache to the imports at the top of studio/backend/core/training/trainer.py.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

clear_gpu_cache is imported at the top of trainer.py (line 35, first name in the module-level from utils.hardware import block), and all five call sites resolve; CI is green on this head. No NameError is possible here.

self._cuda_audio_used = True

if not processed_examples:
Expand Down Expand Up @@ -1744,6 +1749,8 @@ def _preprocess_bicodec_dataset(self, dataset, custom_format_mapping = None):

import subprocess

# Spark-TTS BiCodec has not been validated on Intel XPU; keep the
# pre-PR CPU fallback for non-CUDA hosts.
device = "cuda" if torch.cuda.is_available() else "cpu"

# The sparktts Python package lives in the SparkAudio/Spark-TTS GitHub repo,
Expand Down Expand Up @@ -1944,7 +1951,8 @@ def extract_wav2vec2_features(wavs: torch.Tensor) -> torch.Tensor:
import gc

gc.collect()
torch.cuda.empty_cache()

clear_gpu_cache()
self._cuda_audio_used = True

if not processed_examples:
Expand Down Expand Up @@ -1979,6 +1987,8 @@ def _preprocess_dac_dataset(self, dataset, custom_format_mapping = None):
from datasets import Dataset as HFDataset
from utils.paths import ensure_dir, tmp_root

# OuteTTS DAC/Whisper preprocess has not been validated on Intel
# XPU; keep the pre-PR CPU fallback for non-CUDA hosts.
device = "cuda" if torch.cuda.is_available() else "cpu"

# Clone OuteTTS repo (same as audio_codecs._load_dac)
Expand Down Expand Up @@ -2157,7 +2167,8 @@ def _preprocess_dac_dataset(self, dataset, custom_format_mapping = None):
import gc

gc.collect()
torch.cuda.empty_cache()

clear_gpu_cache()
self._cuda_audio_used = True

if not processed_examples:
Expand Down
10 changes: 9 additions & 1 deletion studio/backend/models/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ class LoadRequest(BaseModel):
)
gpu_ids: Optional[List[int]] = Field(
None,
description = "Physical GPU indices to use, for example [0, 1]. Omit or pass [] to use automatic selection. Explicit gpu_ids are unsupported when the parent CUDA_VISIBLE_DEVICES uses UUID/MIG entries. Not supported for GGUF models.",
description = (
"Physical GPU indices to use, for example [0, 1]. Omit or pass "
"[] to use automatic selection. Explicit gpu_ids are unsupported "
"when the parent visibility mask uses non-numeric or subdevice "
"entries -- this includes CUDA_VISIBLE_DEVICES with UUID/MIG "
"entries on NVIDIA, and ZE_AFFINITY_MASK with subdevice tokens "
"(e.g. '0.0,0.1') or FLAT-hierarchy (default) tile handles on "
"Intel XPU. Not supported for GGUF models."
),
)
speculative_type: Optional[str] = Field(
None,
Expand Down
10 changes: 9 additions & 1 deletion studio/backend/models/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,15 @@ def _compat_split(cls, values: Any) -> Any:
# GPU selection
gpu_ids: Optional[List[int]] = Field(
None,
description = "Physical GPU indices to use, for example [0, 1]. Omit or pass [] to use automatic selection. Explicit gpu_ids are unsupported when the parent CUDA_VISIBLE_DEVICES uses UUID/MIG entries.",
description = (
"Physical GPU indices to use, for example [0, 1]. Omit or pass "
"[] to use automatic selection. Explicit gpu_ids are unsupported "
"when the parent visibility mask uses non-numeric or subdevice "
"entries -- this includes CUDA_VISIBLE_DEVICES with UUID/MIG "
"entries on NVIDIA, and ZE_AFFINITY_MASK with subdevice tokens "
"(e.g. '0.0,0.1') or FLAT-hierarchy (default) tile handles on "
"Intel XPU."
),
)


Expand Down
Loading