Skip to content

feat: run:ai gpu_direct model loading with quantization-aware device maps - #635

Open
khaiwang wants to merge 5 commits into
devfrom
zikai/runai-gpu-direct
Open

feat: run:ai gpu_direct model loading with quantization-aware device maps#635
khaiwang wants to merge 5 commits into
devfrom
zikai/runai-gpu-direct

Conversation

@khaiwang

Copy link
Copy Markdown
Contributor

Summary

  • Add run:ai SafetensorsStreamer as the default loading backend for LanguageModel, with silent fallback to from_pretrained when runai-model-streamer is not installed — zero behavior c
    hange for existing users
  • gpu_direct: copies tensors directly from Run:AI's I/O buffer to the target GPU during streaming, making HF's subsequent .to(device) a no-op
  • Quantization-aware _resolve_device_map: creates HfQuantizer and runs preprocess_model on the meta model before computing the device map, so compute_module_sizes sees correct quantized parameter shapes. Without this, MXFP4/GPT
    Q models get 4× memory overestimates, causing unnecessary CPU offloading on tight GPU configs
  • Includes benchmark suite (benchmark_loading.py) and documentation (README.md)

Benchmark Results (cold cache, 8× A100-80GB PCIe)

Model Type Size HF (s) gpu_direct (s) Speedup
Qwen/Qwen3-8B Dense (2 GPU) 15.3 GB 10.3 5.0 2.0×
Qwen/Qwen3-30B-A3B MoE (8 GPU) 56.9 GB 49.3 23.8 2.1×
openai/gpt-oss-120b MXFP4 quantized (8 GPU) 60.8 GB 49.7 17.7 2.8×

How it works

HF baseline:       disk → mmap page faults → CPU → HF .to(cuda) → GPU
Run:AI gpu_direct:  disk → run:ai buffer → .to(cuda) → GPU → HF .to() [no-op]

The Run:AI streamer uses N concurrent C++ pthreads for disk I/O (no GIL contention). The loader resolves device_map upfront, then copies each tensor to its target GPU as it arrives from the streamer. HF's _materialize_copy().to() becom
es a device/dtype no-op. The streamer's background reads overlap naturally with the blocking .to(cuda) calls.

Usage

from nnsight import LanguageModel

# Default: uses run:ai gpu_direct (silently falls back to from_pretrained)
model = LanguageModel("openai/gpt-oss-120b", device_map="auto", dispatch=True)

# Force HF from_pretrained
model = LanguageModel("openai/gpt-oss-120b", load_format="from_pretrained",
                      device_map="auto", dispatch=True)

# Tune Run:AI I/O concurrency (default 16)
model = LanguageModel("openai/gpt-oss-120b", concurrency=4,
                      device_map="auto", dispatch=True)

Files changed

File Description
src/nnsight/modeling/loader.py New — RunAIShardCache, LazyRunAITensor, build_lazy_state_dict
src/nnsight/modeling/transformers.py _load dispatch, _resolve_device_map with quantizer, _load_streamed
tests/performance/loading/benchmark_loading.py Benchmark suite (hf vs gpu_direct, page cache control, correctness verification)
tests/performance/loading/README.md Architecture docs, usage, benchmark results

Test plan

  • pytest tests/test_tiny.py --device cpu — 18/18 pass
  • Dense model (Qwen3-8B): correctness + performance verified on 2 GPUs
  • MoE model (Qwen3-30B-A3B): correctness + performance verified on 8 GPUs
  • MXFP4 quantized (gpt-oss-120b): correctness + performance verified on 4 and 8 GPUs
  • MXFP4 quantized (gpt-oss-20b): correctness verified on 1 GPU — validates quantization-aware device map fix
  • Fallback: loads correctly without runai-model-streamer installed (uses from_pretrained)

khaiwang and others added 2 commits March 19, 2026 19:35
…ice maps

Add run:ai SafetensorsStreamer as the default loading backend for
LanguageModel, with automatic fallback to from_pretrained when
runai-model-streamer is not installed.

Key features:
- gpu_direct mode: copies tensors directly from Run:AI buffer to target
  GPU during streaming, making HF's _materialize_copy().to() a no-op
- Quantization-aware _resolve_device_map: creates HfQuantizer and runs
  preprocess_model on the meta model so compute_module_sizes sees correct
  quantized parameter shapes (fixes 4x memory overestimate for MXFP4/GPTQ)
- Thread-safe shard cache with per-tensor notification for pipelining
  disk I/O with GPU transfers across HF worker threads
- CUDA expandable_segments enabled automatically to prevent fragmentation

Benchmarks (cold cache, 8x A100-80GB PCIe):
- Qwen3-30B-A3B (57 GB bf16): gpu_direct 2.4x faster than HF
- gpt-oss-120b (61 GB MXFP4): gpu_direct 2.4x faster than HF
- gpt-oss-20b (13 GB MXFP4, 1 GPU): gpu_direct 1.9x faster than HF

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Include benchmark_loading.py (hf vs gpu_direct configs) and README
with architecture diagram, usage examples, and benchmark results
across dense (Qwen3-8B), MoE (Qwen3-30B-A3B), and MXFP4 quantized
(gpt-oss-120b) models.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

khaiwang and others added 3 commits March 19, 2026 21:03
RunAI's DistributedStreamer auto-activates when torch.distributed is
initialized (required by tp_plan), conflicting with the NCCL process
group that transformers' TP creates.  Detect tp_plan in _load and
route to from_pretrained, which handles TP sharding natively
(torch.narrow per rank during weight loading).

Tested: torchrun --nproc_per_node=2 with Qwen3-8B tp_plan="auto"
produces correct output with ~9 GB/rank (half of 15.3 GB model).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RunAI's DistributedStreamer runs find_local_ranks() on every
stream_files() call when torch.distributed is initialized.  This
creates a second NCCL communicator that conflicts with the existing
TP process group, causing "Duplicate GPU detected" errors.

Detect tp_plan in _load and route to from_pretrained, which handles
TP sharding natively (torch.narrow per rank during weight loading).

Tested: torchrun --nproc_per_node=2 with Qwen3-8B tp_plan="auto"
produces correct output with ~9 GB/rank (half of 15.3 GB model).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single-node TP with mmap shares OS page cache across ranks (1× read),
while run:ai CPU-clone reads N× the data independently (measured 2×
slower for Qwen3-8B on 2 GPUs). Also adds ValueError fallback for
repos without .safetensors files and avoids passing device_map with
tp_plan (mutually exclusive in from_pretrained).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant