Skip to content

perf(fastsac): weight-snapshot transport — fused publish + CUDA-IPC slots with size gate - #67

Open
wlgys8 wants to merge 1 commit into
mainfrom
perf/fastsac-weight-ipc
Open

wlgys8 wants to merge 1 commit into
mainfrom
perf/fastsac-weight-ipc

Conversation

@wlgys8

@wlgys8 wlgys8 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

背景

FastSAC async 两进程训练中,权重快照(learner → collector)走 host 共享内存,每个 publish/sync 周期涉及多次设备↔主机搬运:

  • publish:参数逐 tensor 同步 .cpu() + shm 拷贝
  • sync(collector 侧):shm → pinned → H2D 三跳

大 actor(~27M 参数 / ~108MB fp32)下实测单次 publish 高达 ~70ms、sync ~10ms,成为 learner/collector 两侧的共同瓶颈。

改动

1. publish 传输融合(23a66f6

参数在设备侧 flatten 成一个连续向量,单次 pinned D2H 进 host 槽;learner 与 collector 推理同卡时,双缓冲槽直接驻留 GPU(CUDA IPC,启动时经 mp.Queue 一次性握手),publish/sync 变为 device-to-device 拷贝。seqlock 协议不变(seq 计数器仍在 host shm;GPU 拷贝在版本发布前 event-sync 完成,读取侧同步后再复查 seq,防止撕裂)。

2. 传输抽象 + 按体积自动选路(335d229

把槽位传输抽成独立模块 weight_slots.py 的两个策略:

  • HostShmWeightSlots — 共享内存双缓冲 + 单次 pinned 传输(默认)
  • GpuIpcWeightSlots — CUDA-IPC 设备双缓冲 + D2D 拷贝

新增配置:

async_options:
  weight_ipc: auto        # auto / on / off
  weight_ipc_min_bytes: 16777216

auto(默认):同卡且参数 ≥ 16MB 才走 IPC,否则 host 路径。

为什么需要 size gate

基准测试发现 IPC 对小 actor 是负收益(其读写两端的流/事件同步比它们省下的亚毫秒级 host 传输更贵):

场景 actor 参数量 host 路径 IPC 路径
g1-wbt-dance(2048 envs) ~1MB ~76k steps/s ~67–71k ❌
大 actor(2048 envs) ~27M / 108MB ~45k(publish 70ms / sync 10.6ms) ~61k(2.3ms / 0.6ms)✅

auto 让小 actor 保持零回归,大 actor 自动拿到收益。

实现细节

  • 仅槽张量经过 IPC 队列(策略对象持有不可 pickle 的 torch.cuda.Event,collector 侧重建)
  • shared_cpu_tensor 移入 weight_slots,两模块共享同一个零初始化分配器
  • host 路径完整保留为 CPU learner / CPU 推理 / 跨卡的回退

测试

  • fastsac 相关 19 个单测全过(含 seqlock 时序测试、spawn 集成测试)
  • g1-wbt-dance:auto 回落 host 路径(吞吐与 main 持平 ~76k),on 强制设备路径(host_snapshot 归零)验证开关生效

@wlgys8
wlgys8 force-pushed the perf/fastsac-weight-ipc branch from 335d229 to c5f13b3 Compare September 21, 2026 05:02
…lots with size gate

The async learner->collector weight snapshot paid multiple device<->host
crossings per cycle: a per-tensor synchronous .cpu() flatten plus shm
copies on publish, and shm -> pinned -> H2D on every collector sync.
Large actors (~27M params / ~108MB fp32) measured ~70ms per publish and
~10ms per sync, bottlenecking both processes.

- publish flattens params on-device into one contiguous vector and
  crosses to the host in a single pinned transfer.
- when learner and collector inference share one GPU, the double-buffered
  slots live in device memory via CUDA IPC (one mp.Queue handoff at
  startup) and publish/sync become device-to-device copies. The seqlock
  protocol is unchanged: GPU slot writes are event-synced before the
  version bump, and reads synchronize before the seq re-check so a
  republish during an in-flight copy is retried instead of tearing the
  actor.
- the slot transport is extracted into weight_slots.py strategies
  (HostShmWeightSlots / GpuIpcWeightSlots) that WeightSnapshot delegates
  to; host shm remains the fallback for CPU learners, CPU collector
  inference, or mismatched device indices.
- async_options.weight_ipc (auto/on/off, default auto) with
  weight_ipc_min_bytes (16MB) gates the device path: benchmarks show IPC
  is a net loss for small actors (g1-wbt-dance ~1MB params: ~76k ->
  ~71k env-steps/s) because its stream synchronizations cost more than
  the sub-millisecond host transfer they avoid, while large actors gain
  publish ~70ms -> ~2.3ms, sync ~10.6ms -> ~0.6ms and ~45k -> ~61k
  env-steps/s.

Only the slot tensors cross the IPC queue (the strategy object holds an
unpicklable CUDA event); shared_cpu_tensor moves to weight_slots so both
modules share one zero-initialized allocator.
@wlgys8
wlgys8 force-pushed the perf/fastsac-weight-ipc branch from c5f13b3 to 2c6e183 Compare September 21, 2026 05:25
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