intel / intel/torch-xpu-ops

[Bug] GPU monitoring overhead on XPU: fork-based approach causes ~46% latency increase

Open
#4,891 0 comments 0 reactions 1 assignee Claimed by @guangyey View on GitHub
Dominant language
Python
Stars
113
Forks
128
Avg merge
5d 13h
Merged PRs (30d)
107

Description

## Summary

When using `multiprocessing.Process` (fork) for GPU monitoring in benchmarks, XPU inference latency increases by ~46% (~21ms vs ~14ms baseline). Using `threading.Thread` instead eliminates the overhead completely.

## Background

This issue was discovered while investigating GPU monitoring overhead in the [HuggingFace_OOB benchmark framework](https://github.com/intel-sandbox/HuggingFace_OOB) (see [PR #228](https://github.com/intel-sandbox/HuggingFace_OOB/pull/228) for the fix).

The benchmark uses a GPUMonitor class that spawns a child process to collect GPU utilization/memory metrics during inference. On XPU, this caused unexpected ~46% latency overhead.

## Reproducer

```python
#!/usr/bin/env python3
"""Reproduce GPU monitoring overhead on XPU: Process vs Thread.

Environment:
- Machine: G31 (Intel(R) Graphics [0xe223])
- Model: facebook/pe-a-frame-large (feature-extraction)
- Compile: torch.compile(model.forward, mode="default")
- RSS padding: ~2.3GB (torch.randn(600_000_000))
"""

import argparse
import gc
import subprocess
import time
import statistics
import numpy as np
from multiprocessing import Process, Queue, Pipe
import threading

import torch
from transformers import AutoModel, AutoProcessor, is_torch_xpu_available

def _sample_pytorch():
u = int(torch.xpu.utilization())
m = torch.xpu.device_memory_used() / 1024**3
return u, m

class ForkPytorchMonitor:
"""multiprocessing.Process + torch.xpu API."""
def __init__(self):
self.process = None
self.q = Queue()

def start(self):
def worker(q):
try:
while True:
q.put(_sample_pytorch())
time.sleep(0.05)
except:
pass
self.process = Process(target=worker, args=(self.q,), daemon=True)
self.process.start()

def stop_and_collect(self):
if self.process is None:
return []
self.process.terminate()
self.process.join(timeout=2.0)
samples = []
while not self.q.empty():
try:
samples.append(self.q.get_nowait())
except:
break
return samples

class ThreadPytorchMonitor:
"""threading.Thread + torch.xpu API (the fix)."""
def __init__(self):
self.thread = None
self.q = Queue()
self.stop = threading.Event()

def start(self):
def worker():
while not self.stop.is_set():
try:
self.q.put(_sample_pytorch())
except:
pass
time.sleep(0.05)
self.thread = threading.Thread(target=worker, daemon=True)
self.thread.start()

def stop_and_collect(self):
if self.thread is None:
return []
self.stop.set()
self.thread.join(timeout=2.0)
samples = []
while not self.q.empty():
try:
samples.append(self.q.get_nowait())
except:
break
return samples

MONITORS = {
"fork-pytorch": ForkPytorchMonitor,
"thread-pytorch": ThreadPytorchMonitor,
}

def flush_memory(flush_compile=False):
gc.collect()
if flush_compile:
torch._dynamo.reset()
torch._dynamo.reset_code_caches()

def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=list(MONITORS.keys()), required=True)
parser.add_argument("--iters", type=int, default=10)
parser.add_argument("--warmup", type=int, default=3)
args = parser.parse_args()

device = torch.device("xpu")
print(f"Device: {torch.xpu.get_device_name(0)}")
print(f"Mode: {args.mode}")

model_id = "facebook/pe-a-frame-large"
print(f"Loading {model_id}...")
processor = AutoProcessor.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id, dtype=torch.bfloat16).to(device).eval()
model.forward = torch.compile(model.forward, mode="default", fullgraph=False)

feature_extractor = getattr(processor, "feature_extractor", processor)
sample_rate = getattr(feature_extractor, "sampling_rate", None) or 16000
dummy_audio = np.random.randn(sample_rate * 1).astype(np.float32) * 0.01
inputs = feature_extractor(
[dummy_audio], sampling_rate=sample_rate, return_tensors="pt", padding=True,
)

inputs = {
k: v.to(device, dtype=torch.bfloat16) if v.is_floating_point() else v.to(device)
for k, v in inputs.items()
}

rss_pad = torch.randn(600_000_000, device=device, dtype=torch.float32)
print(f"Allocated ~2.3GB padding to inflate RSS")

def timed_forward():
torch.xpu.synchronize()
t0 = time.perf_counter()
with torch.no_grad():
_ = model(**inputs)
torch.xpu.synchronize()
elapsed = (time.perf_counter() - t0) * 1000
flush_memory(flush_compile=False)
return elapsed

for _ in range(args.warmup):
timed_forward()

t_base = [timed_forward() for _ in range(args.iters)]

MonitorClass = MONITORS[args.mode]
t_mon = []
for _ in range(args.iters):
mon = MonitorClass()
mon.start()
lat = timed_forward()
mon.stop_and_collect()
t_mon.append(lat)

def stats(t):
return statistics.median(t), statistics.mean(t), statistics.stdev(t)

mb, ab, sb = stats(t_base)
mm, am, sm = stats(t_mon)

print()
print(f"{chr(39)}Mode{chr(39):<40s} {chr(39)}median{chr(39):>10s} {chr(39)}mean{chr(39):>10s} {chr(39)}std{chr(39):>10s} {chr(39)}overhead{chr(39):>10s}")
print("-" * 80)
print(f"{chr(39)}No monitor (baseline){chr(39):<40s} {mb:>9.2f}ms {ab:>9.2f}ms {sb:>9.2f}ms {chr(39)}---{chr(39):>10s}")
print(f"{args.mode + chr(32) + chr(39) + chr(40) + chr(116) + chr(101) + chr(115) + chr(116) + chr(41) + chr(39):<40s} {mm:>9.2f}ms {am:>9.2f}ms {sm:>9.2f}ms {(mm / mb - 1) * 100:>+9.1f}%")

del rss_pad
flush_memory(flush_compile=True)

if __name__ == "__main__":
main()
```

## Results (G31, Intel(R) Graphics [0xe223])

| Mode | Median Latency | Overhead |
|------|----------------|----------|
| No monitor (baseline) | 14.26ms | — |
| fork-pytorch (Process + torch.xpu) | 20.80ms | +46% |
| fork-xpu-smi (Process + xpu-smi) | 21.33ms | +49% |
| thread-pytorch (Thread + torch.xpu) | 14.20ms | ≈0% |

## Fix

The fix is applied in [PR #228](https://github.com/intel-sandbox/HuggingFace_OOB/pull/228), changing `framework/hardware_metrics.py`:
- `multiprocessing.Process` → `threading.Thread`
- Use `torch.xpu` in-process API instead of subprocess `xpu-smi`

## Environment

- **Machine**: G31 (Intel(R) Graphics [0xe223])
- **Driver**: 26.22.38646.6
- **PyTorch**: 2.10.0+cu131
- **Model**: facebook/pe-a-frame-large (feature-extraction, batch=1, seqlen=1024, compile mode)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.