Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions skills/training/start-run/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ selected environment before starting the run.
when the defaults are insufficient. It does not create a Gloo process group.
Set `cpu_optimizer_backend = "torch"` inside the offload table to use fused
PyTorch AdamW for debugging or parity checks.
- Any `optim_cpu_offload` mode raises the trainer's intra-op thread count at
startup. Launchers export `OMP_NUM_THREADS=1`, which would otherwise leave the
bandwidth-bound CPU AdamW kernels on a single core. Each rank claims
`cpu_count / local_world_size` threads, capped by its affinity mask, so the
ranks on a node never oversubscribe it. This overrides `OMP_NUM_THREADS`, so
setting it in `env_vars` does not change the offloaded optimizer's thread count.

## `rl` — RL training

Expand Down
7 changes: 5 additions & 2 deletions src/prime_rl/trainer/rl/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
begin_backward,
clip_grad_norm_,
bind_process_to_gpu_numa_node,
configure_cpu_optimizer_threads,
export_benchmark_json,
filter_rl_trainer_tensor_stats_for_wandb,
finish_backward,
Expand Down Expand Up @@ -118,8 +119,10 @@ def train(config: TrainerConfig):
setup_torch_distributed(
timeout=timedelta(seconds=config.dist_timeout_seconds), enable_gloo=config.model.fsdp_cpu_offload
)
if config.model.optim_cpu_offload and config.model.optim_cpu_offload.numa_bind:
bind_process_to_gpu_numa_node()
if config.model.optim_cpu_offload:
if config.model.optim_cpu_offload.numa_bind:
bind_process_to_gpu_numa_node()
configure_cpu_optimizer_threads()
# Configurable to support ROCm/AMD GPUs where reduced precision
# matmul corrupts softmax over large vocabularies. Override via config
# (e.g. matmul_precision = "highest") on ROCm.
Expand Down
7 changes: 5 additions & 2 deletions src/prime_rl/trainer/sft/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
MemoryProfiler,
begin_backward,
bind_process_to_gpu_numa_node,
configure_cpu_optimizer_threads,
clip_grad_norm_,
export_benchmark_json,
finish_backward,
Expand Down Expand Up @@ -91,8 +92,10 @@ def train(config: SFTConfig):
setup_torch_distributed(
timeout=timedelta(seconds=config.dist_timeout_seconds), enable_gloo=config.model.fsdp_cpu_offload
)
if config.model.optim_cpu_offload and config.model.optim_cpu_offload.numa_bind:
bind_process_to_gpu_numa_node()
if config.model.optim_cpu_offload:
if config.model.optim_cpu_offload.numa_bind:
bind_process_to_gpu_numa_node()
configure_cpu_optimizer_threads()
# Configurable to support ROCm/AMD GPUs where reduced precision
# matmul corrupts softmax over large vocabularies. Override via config
# (e.g. matmul_precision = "highest") on ROCm.
Expand Down
14 changes: 12 additions & 2 deletions src/prime_rl/trainer/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import gc
import heapq
import json
import os
import pickle
import shutil
import time
Expand Down Expand Up @@ -423,8 +424,6 @@ def bind_process_to_gpu_numa_node() -> None:
GPU hangs off. Must run before CPU optimizer state allocation and before the
OMP thread pool spins up.
"""
import os

import pynvml

logger = get_logger()
Expand Down Expand Up @@ -453,6 +452,17 @@ def bind_process_to_gpu_numa_node() -> None:
logger.info(f"Bound rank with GPU {device_id} to NUMA node {numa_node} ({len(cpus)} CPUs)")


def configure_cpu_optimizer_threads() -> None:
available = os.sched_getaffinity(0)
fair_share = (os.cpu_count() or len(available)) // get_world().local_world_size
threads = max(1, min(len(available), fair_share))
torch.set_num_threads(threads)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thread fair-share oversubscribes shared CPUs

Medium Severity

configure_cpu_optimizer_threads budgets each rank with os.cpu_count() / local_world_size, then caps only by that rank's full affinity size. When several ranks share the same mask — after numa_bind on one socket, or under a shared cpuset — that cap is not divided among peers, so total intra-op threads can far exceed the CPUs in the mask despite the no-oversubscribe goal.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 21b7ea6. Configure here.

get_logger().info(
f"CPU optimizer uses {threads} intra-op threads "
f"({len(available)} CPUs in this rank's affinity mask, {get_world().local_world_size} local ranks)"
)


def setup_torch_distributed(timeout: timedelta = DEFAULT_TIMEOUT, enable_gloo: bool = False):
device_id = get_world().local_rank
torch.cuda.set_device(device_id)
Expand Down
Loading