intel / intel/compute-runtime

[GSD-13038] Regression in 26.09+: dual-GPU workloads eagerly pin a host-RAM mirror of every large device USM allocation on the peer device, exhausting system memory

Open
#953 7 comments 1 reaction 0 assignees View on GitHub
OS: Linux Status: Needs Feedback Type: Bug Type: Regression
Dominant language
C++
Stars
1.4k
Forks
300
PR merge metrics
No merged PRs in 30d

Description

## Summary

On a 2× Arc Pro B70 (Battlemage G31) system running PyTorch DDP (one rank per card,
xccl/oneCCL collectives), every large device-USM segment a rank allocates on its own
card is also registered on its file descriptor to the **peer** card as GTT (GPU-mapped
system memory). On 26.05 these peer registrations are bookkeeping only. On 26.09+
(observed on 26.22) they are **eagerly pinned to physical host pages**: ~17.7 GiB of
system RAM per rank for an ~18.5 GiB VRAM working set — 35.4 GiB total on a 64 GiB
machine. The kernel then hits order-0 page-allocation failures, the desktop swaps out,
and at production scale the workload dies. Downgrading only `intel-compute-runtime`
26.22 → 26.05 (nothing else changed) reduces pinned memory for the identical workload
from **25.9 GiB to 0.67 GiB** with identical throughput.

## Environment

| component | version |
|---|---|
| intel-compute-runtime | **26.22.38646.4 (bad)** / **26.05.37020.3 (good)** |
| GPUs | 2× Intel Arc Pro B70 (Battlemage G31, 8086:e223), 32 GiB VRAM each, PCIe (no XeLink) |
| kernel / driver | Linux 7.1.3 (CachyOS), `xe` |
| level-zero-loader | 1.28.6 |
| intel-graphics-compiler | 2.36.3 |
| intel-gmmlib | 22.10.0 |
| workload stack | PyTorch 2.13.0+xpu, oneCCL 2022.0 (xccl backend), torchrun 2 ranks |
| system RAM | 64 GiB |

## Reproducer

```python
# gtt_repro.py — torchrun --standalone --nproc-per-node=2 gtt_repro.py
import os, time
import torch
import torch.distributed as dist

rank = int(os.environ["RANK"])
local = int(os.environ["LOCAL_RANK"])
dev = torch.device(f"xpu:{local}")
dist.init_process_group("xccl", device_id=dev)

# 8 GiB of device USM on this rank's card, in 1 GiB segments
held = [torch.empty(1024 ** 3 // 4, dtype=torch.float32, device=dev) for _ in range(8)]
# gradient-bucket-sized collective traffic
bucket = torch.ones(25_000_000, device=dev)
for _ in range(50):
dist.all_reduce(bucket)
torch.xpu.synchronize(dev)

print(f"rank {rank} pid {os.getpid()} holding 8 GiB on {dev}; sleeping for inspection", flush=True)
time.sleep(120)
dist.barrier()
dist.destroy_process_group()
```

Inspect while it sleeps:

```sh
for pid in ; do
for fd in /proc/$pid/fdinfo/*; do grep -HE 'drm-resident-(gtt|vram0)' $fd; done
done
free -h # or a kernel with pinned-GPU-memory accounting in /proc/meminfo
```

Observed per-fd accounting for the reproducer (each rank holds 8 GiB on its own card;
note the ~8.9 GiB **GTT on the fd to the peer card**, mirror-imaged between ranks):

```
pid 116400 (rank 0, computes on renderD128)
fd4 /dev/dri/renderD129: drm-resident-gtt: 8913000 KiB drm-resident-vram0: 191268 KiB
fd5 /dev/dri/renderD128: drm-resident-gtt: 642792 KiB drm-resident-vram0: 9355696 KiB
pid 116401 (rank 1, computes on renderD129)
fd4 /dev/dri/renderD129: drm-resident-gtt: 638440 KiB drm-resident-vram0: 9348024 KiB
fd5 /dev/dri/renderD128: drm-resident-gtt: 9011816 KiB drm-resident-vram0: 392456 KiB
```

(The capture above is from 26.05, where the registrations exist but stay unpinned.
On 26.22 the same registrations are backed by pinned host pages — see below.)

## Bad vs good, identical real workload (PPO trainer, ~16 GiB VRAM working set per rank)

| | 26.22 | 26.05 |
|---|---|---|
| peer-fd `drm-resident-gtt` per rank | 12.8 GiB | 13.3 GiB (bookkeeping only) |
| pinned host memory, both ranks (kernel `GPUActive`) | **25.9 GiB** | **0.67 GiB** |
| throughput | ~3.0 s/wave | ~3.0 s/wave |

(`GPUActive` is the CachyOS kernel's pinned-GPU-page accounting in `/proc/meminfo`;
on a mainline kernel the same effect is visible as "used" memory in `free` that no
process RSS accounts for, disappearing when the workload exits.)

At full production scale on 26.22 the pinning reached 35.4 GiB (17.7 GiB per rank),
`free` showed ~7 GiB available of 64 GiB, the kernel logged order-0 allocation
failures in unrelated processes (`kwin_wayland: page allocation failure: order:0`),
and the training run died without OOM-killer involvement.

Growth is stepwise and exactly tracks the allocator's large segments: peer-fd GTT grew
in increments of 3,174,400 KiB — precisely the workload's 512×2048×795 fp32 buffer —
and plateaus at ~the VRAM working set once the caching allocator stops creating new
segments. Nothing in the workload allocates host or shared USM of this size; all
tensors are device-resident.

## What I ruled out

- **USM pooling knobs**: `NEOReadDebugKeys=1 EnableHostUsmAllocationPool=0
EnableDeviceUsmAllocationPool=0 EnableUsmAllocationPoolManager=0` on 26.22 → peer-fd
GTT unchanged; additionally the workload then crashes in backward with
`UR_RESULT_ERROR_OUT_OF_RESOURCES` (possibly its own issue).
- **Workload-side caching**: releasing unused allocator segments
(`torch.xpu.empty_cache()`) after warmup reclaims only the warmup-transient portion
(~2 GiB/rank); live segments remain mirrored and pinned.
- **VRAM oversubscription/eviction**: `drm-resident-vram0` ≈ `drm-total-vram0`
throughout; VRAM is never oversubscribed (≤ 19 of 32 GiB).

## Expected behavior

Cross-device registrations for peer access (if that is what these are) should not
eagerly pin physical host pages proportional to the peer's entire VRAM working set —
26.05's lazy behavior, which performs identically for this workload, demonstrates the
pinning is unnecessary.

## Possibly related

- **#952 looks like this same defect presenting as a crash at 4-GPU scale.** There:
4× B60, 60 GB RAM, ~11.4 GiB reserved per rank, `UR_RESULT_ERROR_OUT_OF_RESOURCES`
at the first large kernel — exactly what eager peer-pinning predicts (each rank's
working set mirrored per peer exceeds host RAM at 4 ranks; our 2-GPU/64 GiB system
survives at 35.4 GiB pinned but dies at the system level instead). Their
discriminating evidence (single-GPU OK, independent concurrent jobs OK, small-model
DDP OK, large-model DDP fails on 2/3/4 ranks) matches peer-mirror scaling, and their
USM-pool debug-key attempts not helping matches ours.
- #935: on these cards (BMG, cross-root-port) L0 dev-to-dev P2P is refused by the
kernel, so peer access is host-backed — presumably why these peer GTT registrations
exist at all. The regression here is only that 26.09+ pins them eagerly.
- The 25.40→26.09 window matches the `useUsmPoolManager` / "l0 device usm growing
pools" default flip referenced in #916 (also a dual-Arc, UR multi-device-context
setup), though disabling those pools via debug keys did not change the behavior
here, so the overlap may be coincidental.

Contributor guide

Open the contributing guide

Research direction

Start by running gtt_repro.py with the stated two-rank torchrun command on compute-runtime 26.22 and inspect each rank's /proc//fdinfo entries while it sleeps. Compare peer-fd drm-resident-gtt and host-memory accounting with 26.05; done means peer registrations no longer eagerly pin host pages proportional to device-USM allocations, without reducing workload throughput.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux, pytorch
Domain
backend, computer-graphics, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.