Multi-host prefix cache: startup row thinning of a recovered disk store overflows the 64-frame record backlog and stops the server
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 42/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- rust
- Ambito
- backend, performance
Direzione di ricerca
Start with thin_rows and maintenance scheduling in selene/src/disk_store.rs, then inspect the backlog handling in selene/src/shard.rs. Reproduce with the supplied recovered-chain test and compare the short and long cases. Done when startup thinning completes without overflowing the peer record backlog and the regression test passes.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Symptom. A TP3 server (one rank per host) whose ranks reuse an existing --prefix-cache-disk directory joins every rank ("4890 of 4890 records on every rank") and starts the HTTP server, then stops immediately:
- rank 0:
native replay did not drain: rank 2's prefix record connection failed while jobs were outstanding: prefix record connection backlog is full; the peer is not keeping up - peer:
host prefix record service did not drain: prefix record connection closed
The same package with an empty directory starts and serves normally.
Root cause.
DiskStore::newseedsthinningwith the parent of every scanned record (selene/src/disk_store.rs:1561).- The first
pollcallsthin_rows(disk_store.rs:2075, fromdisk_store.rs:2617/composite.rs:1324), which compacts every redundant row in one pass with no bound. Each compaction is atruncate(disk_store.rs:2299). - With
Sharded, each truncate is queued to every peer throughLink::enqueue(selene/src/shard.rs:587-591,897-903), whichtry_sends into async_channel(BACKLOG = 64)(shard.rs:29,202,239-266). A full queue marks the link broken andabandonpoisons the transport (shard.rs:756). - The 64-frame bound is on the channel to the link's writer thread, so a single
pollthat queues more than about 64 frames for a peer can overflow before the writer thread drains it, even when the peer reads promptly over loopback. - A directory written before eager thinning (#168) can hold thousands of thinnable interior rows. Ours had 3485 per rank.
Conditions. A multi-host package with --prefix-cache-disk and a recovered directory holding more than about 64 interior records whose rows are redundant (each has one ready child). Not verified: any other path that queues more than 64 frames for a peer within one poll would fail the same way, for example make_room evicting many small leaves for one large store. Max-age expiry is capped at 64 per sweep (shared/index.rs:406), so it could only contribute together with other jobs. A peer that briefly stops reading makes all of these more likely.
Reproduction (CPU only). In the disk_store.rs tests, build a DiskStore over a scanned linear chain of N records, with the files present in both directories, and give it a Sharded transport with one Link to a crate::shard::serve(Peer { .. }) thread over a loopback TcpStream; then call store.poll() in a loop. With cargo test --release, N = 3500 fails 5 out of 5 runs after 65-80 truncates with the two errors above; N = 40-70 passes, and N = 100-400 fails intermittently.
Test (append to the mod tests of selene/src/disk_store.rs)
/// Repro: a store recovered over a linear chain thins every interior row in
/// one `poll`, and over the multi-host transport each truncate becomes one
/// queued frame per peer. With a local peer that reads promptly, a chain
/// longer than `shard::BACKLOG` still poisons the transport.
fn thin_recovered_chain_over_sharded(records: u64) -> std::result::Result<(), String> {
use crate::shard::{Link, Peer, Sharded};
let geometry = geometry();
let full = history(records * PAGE_TOKENS);
let zero_dir = std::env::temp_dir().join(format!("flare-repro-zero-{records}-{}", std::process::id()));
let peer_dir = std::env::temp_dir().join(format!("flare-repro-peer-{records}-{}", std::process::id()));
for d in [&zero_dir, &peer_dir] {
let _ = fs::remove_dir_all(d);
fs::create_dir_all(d).unwrap();
}
let mut scanned_records = Vec::new();
for i in 1..=records {
let parent = (i > 1).then(|| (i - 1) * PAGE_TOKENS);
let header = header(&geometry, &full, i * PAGE_TOKENS, parent);
let length = header.length_with_row();
for d in [&zero_dir, &peer_dir] {
let file = fs::File::create(d.join(record::file_name(&header.key))).unwrap();
file.set_len(length).unwrap();
}
scanned_records.push(scanned(&zero_dir, header, i, true));
}
let staged = || {
Staged::new(
None,
StoppedCopier::shared(&geometry).unwrap(),
DiskThread::start("flare-repro").unwrap(),
)
};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let client = std::net::TcpStream::connect(listener.local_addr().unwrap()).unwrap();
let server = listener.accept().unwrap().0;
let peer = Peer {
staged: staged(),
directory: peer_dir.clone(),
fingerprint: FINGERPRINT,
geometry: geometry.clone(),
link: Link::new(server).unwrap(),
_lock: crate::testing::directory_lock(&peer_dir).unwrap(),
};
let service = std::thread::spawn(move || crate::shard::serve(peer));
let transport = Sharded::new(staged(), vec![Link::new(client).unwrap()]).unwrap();
let mut store = DiskStore::new(
crate::testing::directory_lock(&zero_dir).unwrap(),
IDENTITY,
FINGERPRINT,
1 << 40,
geometry.clone(),
scanned_records,
Box::new(transport),
)
.unwrap();
let mut outcome = Ok(());
let deadline = Instant::now() + Duration::from_secs(20);
while Instant::now() < deadline {
if let Err(error) = store.poll(&mut Vec::new()) {
outcome = Err(error.to_string());
break;
}
if store.stats().rows_thinned[1] == records - 2 && store.maintenance.is_empty() {
break;
}
std::thread::sleep(Duration::from_millis(1));
}
let thinned = store.stats().rows_thinned[1];
let pending = store.maintenance.len();
let closed = store.close();
drop(store);
let served = service.join().unwrap();
eprintln!(
"records={records} thinned={thinned} pending_maintenance={pending} outcome={outcome:?} close={closed:?} peer={served:?}"
);
if outcome.is_ok() && pending != 0 {
outcome = Err(format!("{pending} truncates did not complete"));
}
let _ = fs::remove_dir_all(&zero_dir);
let _ = fs::remove_dir_all(&peer_dir);
outcome
}
#[test]
fn repro_short_recovered_chain_thins_over_sharded_transport() {
// 40 records: 38 interior rows to thin, under the 64-frame backlog.
thin_recovered_chain_over_sharded(std::env::var("REPRO_SHORT").ok().and_then(|v| v.parse().ok()).unwrap_or(40)).unwrap();
}
#[test]
fn repro_long_recovered_chain_thinning_overflows_the_record_backlog() {
// 3500 records (as in the failing directory): 3498 truncates queued for the peer in one poll.
let error = thin_recovered_chain_over_sharded(3500).unwrap_err();
assert!(error.contains("backlog is full"), "{error}");
}
Suggested fix, one or more of:
- Bound
thin_rows(and other maintenance loops) per poll: stop issuing truncates whilemaintenance.len()is at or above a burst limit belowBACKLOG, and keep the remaining keys inthinning. Locally, a limit of 32 makes N = 3500 (and 100, 400) pass in about 0.5 s and keeps the 38 existingdisk_storetests passing. - In
Sharded, keep a per-peer overflow queue and flush it into the link on eachpollinstead of failing onTrySendError::Full, so the bounded channel limits memory rather than correctness; real stalls are still caught byJOB_TIMEOUTand the writer'sREPLY_TIMEOUT. - If failing stays the policy, have the error say that rank 0 queued more than
BACKLOGjobs in one burst rather than "the peer is not keeping up".
Found on main at cc696a6b with DeepSeek V4.1 Flash at TP3 on three DGX Spark (GB10) hosts, after upgrading from a build without eager thinning and keeping its prefix-cache directory.
I have the fix in option 1 with the test above ready as a PR, if that approach works for you; happy to do option 2 instead if you prefer to keep thinning unbounded and make the link absorb bursts.
- Lingua principale
- Rust
- Stelle
- 800
- Fork
- 106
- Merge medio
- 1g 3h
- PR unite (30g)
- 100
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di NVIDIA/hyperflare-inference
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
NVIDIA/hyperflare-inference#268 ·
I maintainer di solito rispondono entro 1 giorno
-
break out engram wait timeApertaarea:caching area:engram enhancement performance triage:design-review
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
NVIDIA/hyperflare-inference#230 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 15/100
NVIDIA/hyperflare-inference#278 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 25/100
NVIDIA/hyperflare-inference#276 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
NVIDIA/hyperflare-inference#271 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di NVIDIA/hyperflare-inference
Issue simili
-
bug UI
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
wardian-app/Wardian#1746 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
jlcodes99/cockpit-tools#3125 ·
I maintainer di solito rispondono entro 2 giorni
-
bug user-sprint
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Vondor/KassenSturz#967 ·
I maintainer di solito rispondono entro 1 giorno
-
area/ci
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
[BUG] Incremental sync warns "no stored highest_uid" on every round for folders with no new mailApertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
rustmailer/bichon#411 ·