Skip to content

Commit dec5138

Browse files
committed
[rl] Add XPU support to GRPO training pipeline
Enable the full GRPO RL pipeline (trainer + vLLM generator) to run on Intel XPU alongside CUDA with minimal, config-driven changes. Key changes: 1. Device detection via ZE_AFFINITY_MASK (always present on Intel GPU systems) -- no runtime torch.xpu.is_available() checks needed. 2. expandable_segments guard: PYTORCH_CUDA_ALLOC_CONF is only set when ZE_AFFINITY_MASK is absent. On XPU the CUDA allocator config corrupts oneDNN USM pointers, causing oneCCL collective failures. 3. Bootstrap callable in PerHostProvisioner sets ZE_AFFINITY_MASK (XPU) or CUDA_VISIBLE_DEVICES (CUDA) inside the child process before Level Zero / CUDA runtime initialization. 4. Generator attention_config field using vLLM's AttentionConfig type. Default (None) preserves existing behavior; XPU config passes AttentionConfig() to let vLLM's platform auto-select the backend. 5. decoder.py: gate separate_full_blocks kwarg on inspect.signature for PyTorch version compatibility. 6. XPU config in config_registry.py: disable max_autotune (avoids OUT_OF_RESOURCES on backward), override FlexAttention class var. 7. Remove xpu_compat.py monkey-patching in favor of the above. Files changed: - torchtitan/models/common/decoder.py (version compat, no behavior change) - torchtitan/experiments/rl/__init__.py (ZE_AFFINITY_MASK guard, lazy imports) - torchtitan/experiments/rl/train.py (bootstrap callable, XPU guards) - torchtitan/experiments/rl/actors/generator.py (attention_config field) - torchtitan/experiments/rl/actors/trainer.py (remove apply_patches) - torchtitan/experiments/rl/examples/alphabet_sort/config_registry.py (XPU config) Verified: 10-step GRPO on 4x Intel Max 1550, reward 0.194 -> 0.351.
1 parent 4b041be commit dec5138

6 files changed

Lines changed: 182 additions & 73 deletions

File tree

torchtitan/experiments/rl/__init__.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,33 @@
3636
import sys
3737
import warnings
3838

39-
# Avoid memory fragmentation and peak reserved memory increasing over time
40-
# To overwrite, set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:False
41-
if "PYTORCH_CUDA_ALLOC_CONF" not in os.environ:
42-
if "torch" in sys.modules:
43-
warnings.warn(
44-
"The 'torch' module has already been imported. "
45-
"Setting PYTORCH_CUDA_ALLOC_CONF may not have an effect."
46-
"For best results, set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True before importing 'torch'."
47-
)
48-
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
39+
# Avoid memory fragmentation and peak reserved memory increasing over time.
40+
# expandable_segments corrupts XPU's oneCCL USM pointers (PyTorch bug: XPU
41+
# allocator reads PYTORCH_CUDA_ALLOC_CONF). Detect XPU via ZE_AFFINITY_MASK.
42+
if "ZE_AFFINITY_MASK" not in os.environ:
43+
if "PYTORCH_CUDA_ALLOC_CONF" not in os.environ:
44+
if "torch" in sys.modules:
45+
warnings.warn(
46+
"The 'torch' module has already been imported. "
47+
"Setting PYTORCH_CUDA_ALLOC_CONF may not have an effect."
48+
"For best results, set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True before importing 'torch'."
49+
)
50+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
4951

50-
from torchtitan.experiments.rl.models.vllm_registry import register_to_vllm
51-
from torchtitan.experiments.rl.models.vllm_wrapper import VLLMModelWrapper
52+
53+
def __getattr__(name: str):
54+
if name == "register_to_vllm":
55+
from torchtitan.experiments.rl.models.vllm_registry import register_to_vllm
56+
57+
return register_to_vllm
58+
if name == "VLLMModelWrapper":
59+
from torchtitan.experiments.rl.models.vllm_wrapper import VLLMModelWrapper
60+
61+
return VLLMModelWrapper
62+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
5263

5364

5465
__all__ = [
5566
"VLLMModelWrapper",
56-
"register_to_vllm", # Export register function for manual use
67+
"register_to_vllm",
5768
]

torchtitan/experiments/rl/actors/generator.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ def _prepare_generation_request_metrics(
143143
inputs.last_token_ts - inputs.first_token_ts
144144
) * 1000
145145
metric_values[f"{prefix}/decode_time_ms"] = first_to_last_token_ms
146-
metric_values[
147-
f"{prefix}/inter_token_latency_ms"
148-
] = first_to_last_token_ms / (inputs.num_generation_tokens - 1)
146+
metric_values[f"{prefix}/inter_token_latency_ms"] = (
147+
first_to_last_token_ms / (inputs.num_generation_tokens - 1)
148+
)
149149

150150
# Emit each value with both Mean and Max aggregators.
151151
return [
@@ -722,6 +722,13 @@ class Config(Configurable.Config):
722722
the new weights. No effect under strict-drain (engine idle at pull time); async hot-swap only.
723723
Default True to avoid reusing stale-weight KV."""
724724

725+
attention_config: AttentionConfig | None = None
726+
"""vLLM attention config passed to the engine. When None (default), the
727+
backend is derived from the model's inner_attention config
728+
(FlexAttention -> FLEX_ATTENTION, VarlenAttention -> CUSTOM).
729+
Platforms that don't support those backends (e.g. XPU) should set this
730+
to AttentionConfig(backend=<supported_backend>)."""
731+
725732
def __post_init__(self):
726733
# The generator runs vLLM full expert parallelism: vLLM forms the EP
727734
# group from all DP*TP ranks, so expert_parallel_degree must equal
@@ -818,6 +825,7 @@ def __init__(
818825

819826
# Build vLLM engine
820827
enable_ep = config.parallelism.expert_parallel_degree > 1
828+
821829
engine_kwargs = dict(
822830
# ``model`` is the path to the HF checkpoint directory. The
823831
# config is sourced from torchtitan's ModelSpec via
@@ -844,7 +852,7 @@ def __init__(
844852
distributed_executor_backend="external_launcher",
845853
gpu_memory_utilization=config.gpu_memory_limit,
846854
enforce_eager=not config.cudagraph.enable,
847-
attention_config=AttentionConfig(
855+
attention_config=config.attention_config or AttentionConfig(
848856
backend=(
849857
AttentionBackendEnum.FLEX_ATTENTION
850858
if isinstance(inner_attn, FlexAttention.Config)
@@ -1128,11 +1136,13 @@ async def _decide_next_action(self) -> LoopDecision:
11281136
# the predicate. In-flight requests keep the predicate true, so they need no notify.
11291137
async with self._engine_loop_condition:
11301138
await self._engine_loop_condition.wait_for(
1131-
lambda: self._close_request is not None
1132-
or self._model_state_dict_pull_request is not None
1133-
or self._queued_generation_requests
1134-
# In-flight requests (on any DP rank) keep rank 0 issuing STEP.
1135-
or self._request_dispatcher.rank0_has_pending_futures()
1139+
lambda: (
1140+
self._close_request is not None
1141+
or self._model_state_dict_pull_request is not None
1142+
or self._queued_generation_requests
1143+
# In-flight requests (on any DP rank) keep rank 0 issuing STEP.
1144+
or self._request_dispatcher.rank0_has_pending_futures()
1145+
)
11361146
)
11371147

11381148
if self._close_request is not None:
@@ -1206,9 +1216,9 @@ async def pull_model_state_dict(self, version: int) -> None:
12061216
self._rank0_check_engine_loop_running("pull_model_state_dict")
12071217

12081218
# A placeholder future for the engine loop to resolve once the pull has been applied.
1209-
pull_model_state_dict_future: asyncio.Future[
1210-
int
1211-
] = asyncio.get_running_loop().create_future()
1219+
pull_model_state_dict_future: asyncio.Future[int] = (
1220+
asyncio.get_running_loop().create_future()
1221+
)
12121222

12131223
# `_engine_loop_condition` wakes the engine loop, if asleep, when a pull is queued.
12141224
async with self._engine_loop_condition:

torchtitan/experiments/rl/actors/trainer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,7 @@ async def forward_backward(
371371
dict[str, float]: Globally-reduced metrics.
372372
"""
373373
logger.debug(
374-
f"{os.getpid()=} PolicyTrainer forward_backward "
375-
f"step {self.policy_version}"
374+
f"{os.getpid()=} PolicyTrainer forward_backward step {self.policy_version}"
376375
)
377376

378377
# RL does not support pipeline parallelism yet, so the trainer

torchtitan/experiments/rl/examples/alphabet_sort/config_registry.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313

1414
import dataclasses
1515

16+
import torch
17+
1618
from torchtitan.components.checkpoint import CheckpointManager
1719
from torchtitan.components.loss import ChunkedLossWrapper
20+
from torchtitan.components.lora import LoRAConverter
1821
from torchtitan.components.lr_scheduler import LRSchedulersContainer
1922
from torchtitan.components.optimizer import default_adamw
2023
from torchtitan.config import (
@@ -24,6 +27,8 @@
2427
ParallelismConfig,
2528
TrainingConfig,
2629
)
30+
from vllm.config import AttentionConfig
31+
2732
from torchtitan.experiments.rl.actors.generator import (
2833
SamplingConfig,
2934
VLLMCudagraphConfig,
@@ -191,6 +196,92 @@ def rl_grpo_qwen3_0_6b_flex() -> Controller.Config:
191196
)
192197

193198

199+
def rl_grpo_lora_qwen3_0_6b() -> Controller.Config:
200+
"""GRPO + LoRA config for Qwen3-0.6B with flex attention (4 GPUs: 2 gen + 2 train).
201+
202+
Uses FSDP (dp_shard=2) instead of TP for training, LoRA adapters (rank=8).
203+
204+
On XPU, set ZE_AFFINITY_MASK for device isolation and
205+
TORCHINDUCTOR_MAX_AUTOTUNE=0 to avoid backward kernel resource exhaustion.
206+
"""
207+
group_size = 8
208+
model_spec = _qwen3_rl_model_registry(
209+
"0.6B",
210+
attn_backend="flex",
211+
converters=[
212+
LoRAConverter.Config(rank=8, alpha=16.0, target_modules=["wqkv", "wo"]),
213+
],
214+
)
215+
# Disable max_autotune for XPU: backward autotuning tries kernel configs
216+
# that exceed XPU register limits (OUT_OF_RESOURCES). Harmless on CUDA
217+
# (just uses the default heuristic config instead of autotuning).
218+
# Must redefine _compiled_flex_attn since torch.compile captures options at
219+
# definition time.
220+
from torch.nn.attention.flex_attention import flex_attention
221+
from torchtitan.models.common.attention import FlexAttention
222+
223+
FlexAttention.inductor_configs = {
224+
**FlexAttention.inductor_configs,
225+
"max_autotune": False,
226+
"coordinate_descent_tuning": False,
227+
}
228+
FlexAttention._compiled_flex_attn = torch.compile(
229+
flex_attention, options=FlexAttention.inductor_configs
230+
)
231+
return Controller.Config(
232+
model_spec=model_spec,
233+
hf_assets_path="torchtitan/experiments/rl/example_checkpoint/Qwen3-0.6B",
234+
async_loop=AsyncLoopConfig(
235+
num_training_steps=10,
236+
num_groups_per_train_step=8,
237+
group_size=group_size,
238+
validation=ValidationConfig(num_samples=20),
239+
batcher=Batcher.Config(
240+
batch=BatchConfig(local_batch_size=2, seq_len=2048),
241+
),
242+
),
243+
compile=CompileConfig(enable=True, backend="aot_eager"),
244+
rollouter=AlphabetSortRollouter.Config(),
245+
renderer=RendererConfig(name="qwen3", enable_thinking=False),
246+
metrics=MetricsProcessor.Config(enable_wandb=False),
247+
trainer=PolicyTrainer.Config(
248+
optimizer=default_adamw(lr=2e-6),
249+
lr_scheduler=LRSchedulersContainer.Config(
250+
warmup_steps=2,
251+
decay_type="linear",
252+
),
253+
training=TrainingConfig(dtype="bfloat16"),
254+
parallelism=ParallelismConfig(
255+
data_parallel_shard_degree=2,
256+
tensor_parallel_degree=1,
257+
),
258+
checkpoint=CheckpointManager.Config(
259+
enable=True,
260+
initial_load_in_hf=True,
261+
interval=10,
262+
last_save_model_only=False,
263+
),
264+
loss=GRPOLoss.Config(),
265+
),
266+
generator=VLLMGenerator.Config(
267+
model_dtype="bfloat16",
268+
gpu_memory_limit=0.85,
269+
cudagraph=VLLMCudagraphConfig(enable=False),
270+
attention_config=AttentionConfig(),
271+
parallelism=InferenceParallelismConfig(
272+
data_parallel_degree=2,
273+
tensor_parallel_degree=1,
274+
),
275+
checkpoint=CheckpointManager.Config(enable=False),
276+
sampling=SamplingConfig(
277+
temperature=0.8,
278+
top_p=0.95,
279+
max_tokens=100,
280+
),
281+
),
282+
)
283+
284+
194285
def rl_grpo_qwen3_0_6b_flex_batch_invariant() -> Controller.Config:
195286
"""GRPO training config for Qwen3-0.6B with flex attention and batch invariance
196287
for bitwise-identical numerics between trainer and generator (4 GPUs: 2 gen + 2 train).

torchtitan/experiments/rl/train.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
import asyncio
2626
import logging
2727
import os
28+
from collections.abc import Callable
2829
from dataclasses import dataclass
2930

3031
# must run before torch import. Set it as early as possible to avoid other
3132
# imports transitively importing torch.
32-
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
33+
# expandable_segments corrupts XPU's oneCCL USM pointers. Detect XPU via ZE_AFFINITY_MASK.
34+
if "ZE_AFFINITY_MASK" not in os.environ:
35+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3336

34-
# TODO: Remove `_src` after monarch cuts a new release. This is already a public
35-
# API in monarch nightly. https://github.com/meta-pytorch/monarch/pull/4327
36-
from monarch._src.actor.host_mesh import default_bootstrap_cmd
3737
from monarch.actor import HostMesh, ProcMesh, this_host
3838

3939
from torchtitan.config import ConfigManager, ParallelismConfig
@@ -52,7 +52,7 @@ def breakable_cudagraph_env(generator_cfg) -> dict[str, str]:
5252
makes prefill attention a cudagraph break (run eager at replay). The import happens before the
5353
generator actor's ``__init__`` and in the vLLM EngineCore worker subprocesses -- which do NOT
5454
inherit a runtime-set ``os.environ`` -- so setting it at runtime is too late. It must go in the
55-
proc's LAUNCH env (the spawn ``bootstrap_command``, or the MAST role.env). Without it the decorator no-ops
55+
proc's LAUNCH env (the spawn bootstrap, or the MAST role.env). Without it the decorator no-ops
5656
and prefill attention is captured as ``output.fill_(0)`` (zeroed) -> the model never reads the
5757
prompt -> coherent-but-unrelated output. FULL_DECODE_ONLY never captures prefill so it needs
5858
nothing. Shared so the OSS spawn path and the fbcode MAST launcher use one source of truth.
@@ -67,21 +67,14 @@ def breakable_cudagraph_env(generator_cfg) -> dict[str, str]:
6767
return {}
6868

6969

70-
def _preimport_torch() -> None:
71-
"""``bootstrap`` setup callable: pre-import torch on the spawned proc.
72-
"""
73-
# TODO: Remove once Monarch/PyTorch fixes concurrent import during unpickling.
74-
import torch # noqa: F401
75-
76-
7770
class PerHostProvisioner:
7871
"""Allocates non-overlapping GPU ranges within a single host.
7972
8073
On the same host, the trainer and generator run on separate GPU
8174
meshes (e.g. GPUs 0-3 for training, GPUs 4-7 for generation). Each
82-
call to `allocate(n)` reserves the next *n* GPUs and returns the launch
83-
env (`CUDA_VISIBLE_DEVICES` plus any `extra_env`) for those GPUs. The
84-
returned env vars are applied to the spawned process via ``bootstrap_command``.
75+
call to `allocate(n)` reserves the next *n* GPUs and returns a
76+
bootstrap callable that sets device visibility env vars before the
77+
accelerator runtime initializes in the spawned process.
8578
"""
8679

8780
def __init__(self, total_gpus: int = 8):
@@ -94,7 +87,7 @@ def available(self) -> int:
9487

9588
def allocate(
9689
self, num_gpus: int, *, extra_env: dict[str, str] | None = None
97-
) -> dict[str, str]:
90+
) -> Callable[[], None]:
9891
if num_gpus > self.available:
9992
raise RuntimeError(
10093
f"Requested {num_gpus} GPUs but only {self.available} "
@@ -103,10 +96,22 @@ def allocate(
10396
gpu_ids = list(range(self.next_gpu, self.next_gpu + num_gpus))
10497
self.next_gpu += num_gpus
10598

106-
env = {"CUDA_VISIBLE_DEVICES": ",".join(str(g) for g in gpu_ids)}
107-
if extra_env:
108-
env.update(extra_env)
109-
return env
99+
def _bootstrap():
100+
gpu_str = ",".join(str(g) for g in gpu_ids)
101+
# Device visibility must be set before torch import initializes
102+
# the accelerator runtime. Detect XPU by checking inherited env
103+
# from the launcher (ZE_AFFINITY_MASK is standard for XPU systems).
104+
if "ZE_AFFINITY_MASK" in os.environ:
105+
os.environ.pop("CUDA_VISIBLE_DEVICES", None)
106+
os.environ["ZE_AFFINITY_MASK"] = gpu_str
107+
else:
108+
os.environ["CUDA_VISIBLE_DEVICES"] = gpu_str
109+
if extra_env:
110+
os.environ.update(extra_env)
111+
# TODO: Remove once Monarch/PyTorch fixes concurrent import during unpickling.
112+
import torch # noqa: F401
113+
114+
return _bootstrap
110115

111116

112117
@dataclass
@@ -151,11 +156,9 @@ def _spawn_proc_mesh(
151156
)
152157
role_gpus_per_node = role_world_size // nodes
153158
provisioner = PerHostProvisioner(total_gpus=gpus_per_node)
154-
env = provisioner.allocate(role_gpus_per_node, extra_env=extra_env)
155159
return host_mesh.spawn_procs(
156160
per_host={"gpus": role_gpus_per_node},
157-
bootstrap=_preimport_torch,
158-
bootstrap_command=default_bootstrap_cmd().with_env(env),
161+
bootstrap=provisioner.allocate(role_gpus_per_node, extra_env=extra_env),
159162
)
160163

161164

@@ -213,24 +216,18 @@ def spawn_proc_mesh(
213216
]
214217
else:
215218
# Single-node mode: partition GPUs on this_host() via
216-
# CUDA_VISIBLE_DEVICES
219+
# device visibility env vars (CUDA_VISIBLE_DEVICES or ZE_AFFINITY_MASK)
217220
host_mesh = this_host()
218221
provisioner = PerHostProvisioner(total_gpus=total_gpus)
219222
trainer_mesh = host_mesh.spawn_procs(
220223
per_host={"gpus": trainer_world_size},
221-
bootstrap=_preimport_torch,
222-
bootstrap_command=default_bootstrap_cmd().with_env(
223-
provisioner.allocate(trainer_world_size)
224-
),
224+
bootstrap=provisioner.allocate(trainer_world_size),
225225
)
226226
generator_meshes = [
227227
host_mesh.spawn_procs(
228228
per_host={"gpus": per_generator_world_size},
229-
bootstrap=_preimport_torch,
230-
bootstrap_command=default_bootstrap_cmd().with_env(
231-
provisioner.allocate(
232-
per_generator_world_size, extra_env=generator_env
233-
)
229+
bootstrap=provisioner.allocate(
230+
per_generator_world_size, extra_env=generator_env
234231
),
235232
)
236233
for _ in range(num_generators)

0 commit comments

Comments
 (0)