Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5a01732
tools/eval: add efficiency evaluation (bare vs yeto, verl and Miles)
chowhsu Oct 20, 2026
8358c1b
syncer: pipeline policy-sweep fragments and async per-round checkpoints
chowhsu Oct 20, 2026
261ba2e
tools/eval: bare Miles trains with unfused attention (match yeto gene…
chowhsu Oct 20, 2026
eb9e229
syncer: bf16 sweep pushes and sweep-aligned checkpoint intervals
chowhsu Oct 20, 2026
c3c9d09
tools/eval: yeto+verl async arm passes the staleness-2 TIS algorithm …
chowhsu Oct 20, 2026
1a783d0
syncer: int8 sweep pushes, bf16 downlink deltas, sync/train overlap
chowhsu Oct 20, 2026
63dc064
tools/eval: bare verl arms run the unpatched fork; async uses merged …
chowhsu Oct 20, 2026
8af78fa
overlap: keep rollout policy age 0, relabel rebased cuts, push whole …
chowhsu Oct 20, 2026
052c31e
tools/eval: parse bare verl fully_async rows by training step; rates …
chowhsu Oct 20, 2026
c9245e5
tools/eval: bare Miles arms do not shuffle prompts (yeto's Miles argv…
chowhsu Oct 20, 2026
73442ad
verl adapter: fully_async keeps NCCL host buffers on /dev/shm in ever…
chowhsu Oct 20, 2026
0122f4e
tools/eval: read the yeto+Miles launcher exit code into the parsed run
chowhsu Oct 20, 2026
152374b
test: overlap island with bf16 downlink deltas against the real syncer
chowhsu Oct 20, 2026
e0f833d
miles adapter: opt-in gradient-accumulation fusion; generic recipe tr…
chowhsu Oct 20, 2026
6a4263e
tools/eval: yeto+Miles arm takes the bare Miles recipe settings
chowhsu Oct 20, 2026
ff893bc
tools/eval: read only the run's own tapes (skip the repo copy and the…
chowhsu Oct 20, 2026
ba17953
tools/eval: verl fully_async generation time from the rollouter, else…
chowhsu Oct 20, 2026
a4b1104
eff eval: final round cut only for yeto+Miles; verl async generation …
chowhsu Oct 21, 2026
77ce9cc
verl: hash the LoRA on the trainer for the single-island publish fast…
chowhsu Oct 21, 2026
74016eb
round cut: write before the trainer offloads; commit the store in the…
chowhsu Oct 21, 2026
3b00fe5
fully_async cut: record untrained prompts without stopping the worker
chowhsu Oct 21, 2026
84d43ce
golden: regenerate decoupling samples for the explicit Miles dropout 0.0
chowhsu Oct 21, 2026
36eb805
driver: time the rollout offload after generation as its own phase
chowhsu Oct 21, 2026
dfe8b91
eff eval: --checkpoint final|every-round for the yeto+Miles arm
chowhsu Oct 21, 2026
bc8f53b
syncer: send each fragment's FINAL version as an exact FULL broadcast
chowhsu Oct 21, 2026
8ef1018
round cut: write the trainer shards in the background for periodic cuts
chowhsu Oct 21, 2026
de275ef
round cut: skip a periodic cut while the previous commit runs; time t…
chowhsu Oct 21, 2026
77b460c
cut digests: hash tensor buffers without a bytes copy
chowhsu Oct 21, 2026
86d8872
round cut: a periodic cut does not wait for the store commit it starts
chowhsu Oct 21, 2026
0f615ab
rollout metadata hook: time its steps (E1 diagnosis)
chowhsu Oct 21, 2026
427d142
fully_async: estimate the cross-version IS truncation off the critica…
chowhsu Oct 21, 2026
4015cc5
test: the fully_async round report fake takes build_metadata's new ke…
chowhsu Oct 21, 2026
1502e7d
round cuts: commit the deferred cut fully off the critical path
chowhsu Oct 21, 2026
ae82993
driver: take the terminal load probe off the round's critical path
chowhsu Oct 21, 2026
31a951a
round cuts: collect a skipped prepared cut's writer at the next cut
chowhsu Oct 21, 2026
0a11aa1
round cuts: do not wake an offloaded trainer for a declined periodic cut
chowhsu Oct 21, 2026
f19a055
eval: give the yeto+Miles arm the bare arms' container resources
chowhsu Oct 21, 2026
19e595a
round cuts: sync a deferred cut's state dir as it was at the cut
chowhsu Oct 21, 2026
3dcbec8
wan: per-stream stall timeout, rate-aware chunk striping, sync diagno…
chowhsu Oct 21, 2026
8154509
round cuts: copy a background cut's state on the device first
chowhsu Oct 21, 2026
13d53ab
publish fast path: stage the digest's tensors with one GPU wait
chowhsu Oct 21, 2026
19f2648
rollout metadata hook: read each sink value once per hook run
chowhsu Oct 21, 2026
4a1c245
eval: tell an NVIDIA A10 from an A10G
chowhsu Oct 21, 2026
1fde82f
test: compare staged digests without their timing fields
chowhsu Oct 21, 2026
d583c59
round cuts: keep a sleeping trainer's cut on the host path
chowhsu Oct 21, 2026
2f3d79c
round cuts: serialize and hash background shards in a writer process
chowhsu Oct 21, 2026
27f66dd
launcher: pin a Modal island to one GPU model with --modal-gpu-name
chowhsu Oct 21, 2026
d8d159c
publish fastpath: hash the policy while a colocated trainer offloads
chowhsu Oct 21, 2026
2509fde
wan: replay INIT after a mid-init redial, link probe with container r…
chowhsu Oct 21, 2026
329472f
Merge s26-e3-sync into s26-e11-cluster
chowhsu Oct 21, 2026
87c298a
eval efficiency: spec.cards sets the per-run card total (E11 whole-cl…
chowhsu Oct 21, 2026
823b2e8
wan: refuse the link probe with the elastic syncer; test a lost socke…
chowhsu Oct 21, 2026
140aefa
eval efficiency: E11 preset and the 2-island yeto+Miles launcher args
chowhsu Oct 21, 2026
b0dfb1c
eval efficiency: JSONL datasets, a train-row cap and local task files…
chowhsu Oct 21, 2026
392368b
eval efficiency: derived Miles model scripts and the yeto+verl rollou…
chowhsu Oct 21, 2026
5593787
eval efficiency: the E11 2-island arm uses elastic island scheduling
chowhsu Oct 21, 2026
ae1a26e
eval efficiency: cluster.py, reward against samples and time for the …
chowhsu Oct 21, 2026
f1b26ff
Merge commit '823b2e82' into s26-e11-cluster
chowhsu Oct 21, 2026
fb520a6
Full-parameter no-sync single island and the E11 0.6B full-parameter …
chowhsu Oct 21, 2026
8783b25
eval efficiency: no-progress watchdog and the E11 full-parameter 2-is…
chowhsu Oct 21, 2026
189df10
E5: time the policy release phases; optional fused LoRA stage
chowhsu Oct 21, 2026
f183d75
E4: trainer driver phase seconds and buffer wait share in the scale r…
chowhsu Oct 21, 2026
72adc7c
E4 tool: merge launch-log events; a preempted round with no groups is…
chowhsu Oct 22, 2026
dd88b26
E5: a release writer keeps the LATEST it wrote and commits once
chowhsu Oct 22, 2026
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
33 changes: 32 additions & 1 deletion docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ or allocation overflows are errors rather than panics.

| type | name | direction | payload |
|---:|---|---|---|
| 1 | HELLO | learner → syncer | protocol_version:u16 (=4), learner_id:u32, connection_generation:u64, dtype:u8 (1=f32, 2=bf16, 3=q4), num_fragments:u32, per-fragment layout, layout_fingerprint:[u8;32], session_contract_hash:[u8;32], optional syncer_profile_hash:[u8;32], num_streams:u16, optional compat_group trailer (`b"YCG1"`, u32 length, 1–64 bytes utf-8) |
| 1 | HELLO | learner → syncer | protocol_version:u16 (=4), learner_id:u32, connection_generation:u64, dtype:u8 (1=f32, 2=bf16, 3=q4, 4=f32 with bf16 pushes, 5=f32 with int8 pushes; flag 0x80 = bf16 downlink deltas), num_fragments:u32, per-fragment layout, layout_fingerprint:[u8;32], session_contract_hash:[u8;32], optional syncer_profile_hash:[u8;32], num_streams:u16, optional compat_group trailer (`b"YCG1"`, u32 length, 1–64 bytes utf-8) |
| 2 | INIT_PARAMS | learner → syncer | fragment_id:u32, full tensor bytes; only learner 0 may initialize |
| 3 | PULL_REQ | syncer → learner | fragment_id:u32, global_step:u64, round_attempt:u32 |
| 4 | PUSH_FRAGMENT | learner → syncer | learner_id:u32, fragment_id:u32, global_step:u64, round_attempt:u32, base_version:u64, local_step:u64, c_steps:u32, c_tokens:u64, base-relative learner-delta bytes |
Expand Down Expand Up @@ -223,6 +223,37 @@ The coordinator is one sequential scheduler actor. Pipelined rounds target
distinct fragments, and merge + version update + broadcast for a completion
is serialized. No two completions mutate one fragment concurrently.

## bf16 pushes over an f32 session (dtype 4)

Dtype 4 keeps INIT, BCAST and FINAL exact f32 and sends only PUSH deltas as
bf16 (round-to-nearest-even). The syncer decodes and negates them like dtype 2
and keeps f32 masters and momentum. The full-parameter policy sweep accepts
dtype 1 or 4 (S26 E3, `--rl-sync-push-dtype bf16`).

## int8 pushes over an f32 session (dtype 5)

Dtype 5 sends PUSH deltas as block-scaled int8: u32 block size B
(32..1048576), then per block of B values (the last block may be shorter) an
f32 scale (block absmax / 127) and one i8 per value (`-128` is invalid).
A value decodes to `q * scale`, so its error is at most absmax / 254. The
learner picks B (launcher `--rl-sync-push-int8-block`, default 128); it is
bound into the role-stream session contract. INIT, BCAST and FINAL stay f32.

## bf16 downlink deltas (dtype flag 0x80)

On an f32-bulk session (dtype 1, 4 or 5) the flag 0x80 changes BCAST_FRAGMENT
to `fragment_id:u32, version:u64, kind:u8, base_version:u64,
anchor_sha256:[u8;32]`, then the tensor: kind 0 (FULL) carries f32 values,
kind 1 (DELTA) carries `bf16(theta - anchor)`. Syncer and learner hold the
same f32 anchor per fragment: FULL sets it, DELTA adds the decoded delta in
f32; the digest is checked after every update. Every N-th version of a
fragment is FULL (`--bcast-delta-full-every N`, N >= 2; the syncer refuses a
flagged session without it and the reverse). Initial and reconnect re-sends
are always FULL with the shared anchor. A learner with a missing or wrong
anchor drops its connection, and the reconnect re-send restores it. The
syncer's masters, momentum, checkpoints and FINAL are unchanged and exact;
anchors are not checkpointed, so a resumed syncer starts with FULL.

## Q4 pushes

Q4 uses the same learner-delta semantics. INIT and BCAST stay bf16; PUSH is
Expand Down
302 changes: 302 additions & 0 deletions syncer/src/downlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
//! bf16 downlink deltas for BCAST_FRAGMENT (S26 E3, session flag
//! `DTYPE_FLAG_BCAST_DELTA`).
//!
//! Every learner of a session holds, per fragment, an f32 *anchor* that is
//! bit-identical to the syncer's copy here. A broadcast of a new version is
//! either
//!
//! * `FULL`: the f32 values; the anchor becomes exactly those values, or
//! * `DELTA`: `d = bf16(θ − anchor)`; both sides set `anchor += d` in f32
//! (IEEE addition is exact-rounded, so both sides get the same bits).
//!
//! Each delta is taken against the current anchor, so the rounding error does
//! not accumulate: `|θ − anchor| <= |θ − anchor_prev| * 2^-9` per element
//! after every delta. The learner trains from the anchor; its next push is
//! relative to the anchor, and the syncer adds that push to its exact θ, so
//! θ stays the exact sum of all pushes.
//!
//! Drift guard: every `full_every`-th broadcast of a fragment is FULL; a
//! re-sent fragment (HELLO/reconnect recovery) is always FULL; and every
//! header carries the SHA-256 of the f32 anchor after the update. A learner
//! whose anchor version or digest does not match drops its connection; the
//! reconnect recovery then sends FULL.
//!
//! Anchors live only in memory: a resumed syncer starts with none, so its
//! first broadcast of every fragment is FULL.
//!
//! Header after the usual `fragment_id u32, version u64` prefix:
//! `kind u8 (0 FULL, 1 DELTA), base_version u64, anchor_sha256 [32]`.
//! The tensor that follows is f32 (FULL) or bf16 (DELTA).

use std::sync::Arc;

use anyhow::{bail, Result};
use sha2::{Digest, Sha256};

use crate::protocol::{DTYPE_BF16, DTYPE_F32};

pub const KIND_FULL: u8 = 0;
pub const KIND_DELTA: u8 = 1;
/// fragment_id + version + kind + base_version + digest.
pub const PREFIX_LEN: usize = 4 + 8 + 1 + 8 + 32;

struct Anchor {
version: u64,
values: Arc<Vec<f32>>,
digest: [u8; 32],
deltas_since_full: u64,
}

/// One encoded BCAST: header prefix plus tensor values in `dtype` (DELTA
/// values are already bf16-representable, so encoding them is exact).
pub struct EncodedBcast {
pub prefix: Vec<u8>,
pub dtype: u8,
pub values: Arc<Vec<f32>>,
}

impl EncodedBcast {
#[cfg(test)]
pub fn kind(&self) -> u8 {
self.prefix[12]
}
}

pub struct DownlinkAnchors {
full_every: u64,
anchors: Vec<Option<Anchor>>,
}

pub fn digest_f32(values: &[f32]) -> [u8; 32] {
let mut hasher = Sha256::new();
// Hash in bounded slices to avoid a second full-size byte buffer.
let mut buf = Vec::with_capacity(4 * 65536);
for chunk in values.chunks(65536) {
buf.clear();
for v in chunk {
buf.extend_from_slice(&v.to_le_bytes());
}
hasher.update(&buf);
}
hasher.finalize().into()
}

fn prefix(fragment: usize, version: u64, kind: u8, base_version: u64, digest: &[u8; 32]) -> Vec<u8> {
let mut out = Vec::with_capacity(PREFIX_LEN);
out.extend_from_slice(&(fragment as u32).to_le_bytes());
out.extend_from_slice(&version.to_le_bytes());
out.push(kind);
out.extend_from_slice(&base_version.to_le_bytes());
out.extend_from_slice(digest);
out
}

impl DownlinkAnchors {
pub fn new(fragments: usize, full_every: u64) -> Result<Self> {
if full_every < 2 {
bail!("--bcast-delta-full-every must be at least 2");
}
Ok(Self {
full_every,
anchors: (0..fragments).map(|_| None).collect(),
})
}

/// Encode the broadcast of a newly committed `version` of fragment `p`
/// and advance the shared anchor. Call exactly once per new version.
pub fn publish(&mut self, p: usize, version: u64, params: &[f32]) -> Result<EncodedBcast> {
let slot = self
.anchors
.get_mut(p)
.ok_or_else(|| anyhow::anyhow!("downlink anchor for unknown fragment {p}"))?;
let delta_ok = matches!(
slot,
Some(a) if a.deltas_since_full + 1 < self.full_every
&& a.values.len() == params.len()
&& a.version < version
);
if !delta_ok {
return Ok(Self::reset(slot, p, version, params));
}
let anchor = slot.as_mut().unwrap();
let base_version = anchor.version;
let values = Arc::make_mut(&mut anchor.values);
let mut delta = Vec::new();
delta
.try_reserve_exact(params.len())
.map_err(|error| anyhow::anyhow!("cannot allocate downlink delta: {error}"))?;
for (a, t) in values.iter_mut().zip(params) {
let d = half::bf16::from_f32(t - *a).to_f32();
if !d.is_finite() {
// bf16 overflow of a huge step: fall back to FULL.
return Ok(Self::reset(slot, p, version, params));
}
delta.push(d);
*a += d;
}
anchor.version = version;
anchor.digest = digest_f32(values);
anchor.deltas_since_full += 1;
Ok(EncodedBcast {
prefix: prefix(p, version, KIND_DELTA, base_version, &anchor.digest),
dtype: DTYPE_BF16,
values: Arc::new(delta),
})
}

/// Encode a newly committed `version` of fragment `p` as FULL (exact f32)
/// and reset the shared anchor to it.
pub fn publish_full(&mut self, p: usize, version: u64, params: &[f32]) -> EncodedBcast {
match self.anchors.get_mut(p) {
Some(slot) => Self::reset(slot, p, version, params),
None => EncodedBcast {
prefix: prefix(p, version, KIND_FULL, version, &digest_f32(params)),
dtype: DTYPE_F32,
values: Arc::new(params.to_vec()),
},
}
}

/// FULL re-send for recovery (HELLO / reconnect): the anchor every other
/// learner holds, when it is at `version`; otherwise the exact params.
pub fn resend(&self, p: usize, version: u64, params: &[f32]) -> EncodedBcast {
match self.anchors.get(p).and_then(Option::as_ref) {
Some(anchor) if anchor.version == version => EncodedBcast {
prefix: prefix(p, version, KIND_FULL, version, &anchor.digest),
dtype: DTYPE_F32,
values: Arc::clone(&anchor.values),
},
_ => EncodedBcast {
prefix: prefix(p, version, KIND_FULL, version, &digest_f32(params)),
dtype: DTYPE_F32,
values: Arc::new(params.to_vec()),
},
}
}

fn reset(slot: &mut Option<Anchor>, p: usize, version: u64, params: &[f32]) -> EncodedBcast {
let values = Arc::new(params.to_vec());
let digest = digest_f32(&values);
*slot = Some(Anchor {
version,
values: Arc::clone(&values),
digest,
deltas_since_full: 0,
});
EncodedBcast {
prefix: prefix(p, version, KIND_FULL, version, &digest),
dtype: DTYPE_F32,
values,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

/// A learner-side mirror: applies broadcasts exactly as yeto/protocol.py.
fn apply(anchor: &mut Option<(u64, Vec<f32>)>, enc: &EncodedBcast) -> Result<()> {
let version = u64::from_le_bytes(enc.prefix[4..12].try_into().unwrap());
let base = u64::from_le_bytes(enc.prefix[13..21].try_into().unwrap());
let digest: [u8; 32] = enc.prefix[21..53].try_into().unwrap();
let next = match enc.kind() {
KIND_FULL => enc.values.to_vec(),
KIND_DELTA => {
let Some((held, values)) = anchor.as_ref() else { bail!("no anchor") };
if *held != base {
bail!("anchor version {held} != base {base}");
}
values.iter().zip(enc.values.iter()).map(|(a, d)| a + d).collect()
}
_ => bail!("bad kind"),
};
if digest_f32(&next) != digest {
bail!("digest mismatch");
}
*anchor = Some((version, next));
Ok(())
}

fn params(seed: u64, n: usize) -> Vec<f32> {
(0..n)
.map(|i| ((i as f64 * 0.37 + seed as f64 * 1.3).sin() * 3.0) as f32)
.collect()
}

#[test]
fn deltas_track_params_within_bf16_error_and_refresh_every_m() {
let mut syncer = DownlinkAnchors::new(1, 4).unwrap();
let mut learner = None;
let mut theta = params(0, 1000);
let mut kinds = vec![];
for version in 1..=9u64 {
let step = params(version, 1000);
let prev_anchor = learner.as_ref().map(|(_, v): &(u64, Vec<f32>)| v.clone());
for (t, s) in theta.iter_mut().zip(&step) {
*t += 0.01 * s;
}
let enc = syncer.publish(0, version, &theta).unwrap();
kinds.push(enc.kind());
apply(&mut learner, &enc).unwrap();
let (_, held) = learner.as_ref().unwrap();
if enc.kind() == KIND_FULL {
assert_eq!(held, &theta);
} else {
let prev = prev_anchor.unwrap();
for ((h, t), a) in held.iter().zip(&theta).zip(&prev) {
// one bf16 rounding of (θ − anchor_prev): rel. 2^-8 bound
assert!((h - t).abs() <= (t - a).abs() / 256.0 + 2.0 * f32::EPSILON * t.abs());
}
}
}
assert_eq!(
kinds,
vec![KIND_FULL, KIND_DELTA, KIND_DELTA, KIND_DELTA, KIND_FULL, KIND_DELTA, KIND_DELTA, KIND_DELTA, KIND_FULL]
);
}

#[test]
fn resend_is_the_shared_anchor_and_lets_a_reconnected_learner_continue() {
let mut syncer = DownlinkAnchors::new(1, 100).unwrap();
let mut a = None;
let theta1 = params(1, 64);
apply(&mut a, &syncer.publish(0, 1, &theta1).unwrap()).unwrap();
let theta2: Vec<f32> = theta1.iter().map(|v| v + 0.123).collect();
apply(&mut a, &syncer.publish(0, 2, &theta2).unwrap()).unwrap();
// A fresh learner (reconnect) gets the anchor, not θ, as FULL.
let mut b = None;
let full = syncer.resend(0, 2, &theta2);
assert_eq!(full.kind(), KIND_FULL);
apply(&mut b, &full).unwrap();
assert_eq!(a, b);
let theta3: Vec<f32> = theta2.iter().map(|v| v - 0.05).collect();
let enc = syncer.publish(0, 3, &theta3).unwrap();
assert_eq!(enc.kind(), KIND_DELTA);
apply(&mut a, &enc).unwrap();
apply(&mut b, &enc).unwrap();
assert_eq!(a, b);
}

#[test]
fn a_learner_with_a_wrong_anchor_detects_it() {
let mut syncer = DownlinkAnchors::new(1, 100).unwrap();
let mut a = None;
apply(&mut a, &syncer.publish(0, 1, &params(1, 32)).unwrap()).unwrap();
// Corrupt the learner's anchor bits: the digest check must fail.
if let Some((_, v)) = a.as_mut() {
v[3] += 1e-3;
}
let enc = syncer.publish(0, 2, &params(2, 32)).unwrap();
assert!(apply(&mut a, &enc).unwrap_err().to_string().contains("digest"));
// A learner that skipped a version fails the base-version check.
let mut stale = Some((0u64, params(1, 32)));
let enc = syncer.publish(0, 3, &params(3, 32)).unwrap();
assert!(apply(&mut stale, &enc).unwrap_err().to_string().contains("base"));
}

#[test]
fn full_every_below_two_is_refused() {
assert!(DownlinkAnchors::new(1, 1).is_err());
}
}
12 changes: 12 additions & 0 deletions syncer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod elastic;
mod elastic_server;
mod iso_worker;
mod merge;
mod downlink;
mod protocol;
mod server;
mod state;
Expand Down Expand Up @@ -98,6 +99,15 @@ struct Args {
/// Write the snapshot every N outer steps (0 disables).
#[arg(long, default_value_t = 8)]
checkpoint_every: u64,
/// Policy sweep: write per-round checkpoints off the scheduler; each
/// round is broadcast only after its checkpoint is durable.
#[arg(long, default_value_t = false)]
async_checkpoint: bool,
/// Learners whose session asks for bf16 downlink deltas get a full f32
/// BCAST every N-th version of a fragment (N >= 2). Unset: such
/// sessions are refused.
#[arg(long)]
bcast_delta_full_every: Option<u64>,
/// Resume from --checkpoint-path if it exists.
#[arg(long, default_value_t = false)]
resume: bool,
Expand Down Expand Up @@ -273,6 +283,8 @@ fn main() -> anyhow::Result<()> {
final_state: args.final_state,
checkpoint_path: args.checkpoint_path,
checkpoint_every: args.checkpoint_every,
async_checkpoint: args.async_checkpoint,
bcast_delta_full_every: args.bcast_delta_full_every,
resume: args.resume,
mark_final_checkpoint: args.mark_final_checkpoint,
learner_budget_steps: args.learner_budget_steps,
Expand Down
Loading
Loading