Skip to content

Commit 5dbc071

Browse files
perf(native): coalesce exact duplicate batch jobs
1 parent 7122e03 commit 5dbc071

2 files changed

Lines changed: 232 additions & 8 deletions

File tree

docs/PERF_LEDGER.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,38 @@ independent load split. Both A/A medians must lie in `[0.98, 1.02]`
4949
inclusive; a null CI need not straddle `1.0`, and its widest edge from `1.0`
5050
calibrates the retained 2x margin. `cv` remains provenance only.
5151

52+
## 2026-08-01 — **NON-CAMPAIGN / INFORMATIONAL** — exact duplicate batch coalescing
53+
54+
**No competitive claim.** `transcribe_samples_batch` now fingerprints equal
55+
`DecodeParams` plus the exact IEEE-754 bits of each input, verifies every hash
56+
match bit-for-bit, and performs one physical transcription for each duplicate
57+
group. Successful output is restored to every original position; cached
58+
followers report zero physical work. `FW_BATCH_COALESCE=0` is the rollback and
59+
same-binary control.
60+
61+
**Whole-process routing.** On `threadripperje` (64C/128T), eight copies of the
62+
same JFK WAV pinned to 64 logical CPUs took `1.47 s` with coalescing and
63+
`4.01 s` with it disabled (`2.7279x` same-binary raw ratio). Serialized segment
64+
SHA-256 remained
65+
`19136b99d41a68d5075cf7e50b554dd4471d133169658188b3a17daf3e782d2b`;
66+
physical work fell from eight encodes, eight prefills, and 208 token steps to
67+
one encode, one prefill, and 26 token steps.
68+
69+
In one additional 32-thread whole-job invocation, franken took `1.45 s` and
70+
the live whisper.cpp incumbent took `24.43 s`, a raw incumbent/franken ratio of
71+
`16.8483x`; all eight transcripts matched. This number is routing evidence
72+
only: the formal harness rejected the host at preflight because peer processes
73+
violated quiescence, so it emitted no timed verdict.
74+
75+
**Identity and retry predicate.** Candidate ELF
76+
`fb9e99d4214bc3bc5241bdf85780449f7252dfa02330af4abe7727ff2431ba68`;
77+
whisper.cpp ELF
78+
`73cafc3ab406c8c917e402bf1cb8365eda72f147b3489aba33c4db7dff1a9f10`;
79+
model `1fc70f774d38eb169993ac391eea357ef47c88757ef72ee5943879b7e8e2bc69`;
80+
audio `59dfb9a4acb36fe2a2affc14bacbee2920ff435cb13cc314a08c13f66ba7860e`.
81+
Retry the formal batch-eight invocation only when every external process stays
82+
below `0.1` core throughout both arms.
83+
5284
## 2026-07-31 — **NON-CAMPAIGN / INFORMATIONAL** — shared-model multi-file work stealing
5385

5486
**No competitive claim.** This row records routing evidence for the new opt-in

src/native_engine/decode.rs

Lines changed: 200 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ impl LoadedModel {
183183
}
184184

185185
/// Decoding parameters for [`transcribe_samples`] (greedy, temperature 0).
186-
#[derive(Debug, Clone, Default)]
186+
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
187187
pub struct DecodeParams {
188188
/// Source language code (e.g. `"en"`). `None` triggers auto-detection on
189189
/// multilingual models; ignored by English-only models.
@@ -2990,6 +2990,95 @@ pub fn transcribe_samples(
29902990
})
29912991
}
29922992

2993+
/// Exact-equivalent batch jobs that can share one physical transcription.
2994+
struct BatchJobGroup {
2995+
representative: usize,
2996+
members: Vec<usize>,
2997+
}
2998+
2999+
/// Fast, non-cryptographic fingerprint over the exact IEEE-754 sample bits.
3000+
/// Hash matches are always verified bit-for-bit before work is shared, so a
3001+
/// collision can only cost an equality scan; it cannot alias two audio jobs.
3002+
fn batch_audio_fingerprint(samples: &[f32]) -> u64 {
3003+
let mut hash = 0x9e37_79b9_7f4a_7c15_u64 ^ samples.len() as u64;
3004+
for &sample in samples {
3005+
let word = u64::from(sample.to_bits()).wrapping_mul(0xbf58_476d_1ce4_e5b9);
3006+
hash ^= word;
3007+
hash = hash.rotate_left(27).wrapping_mul(0x94d0_49bb_1331_11eb);
3008+
}
3009+
hash ^= hash >> 30;
3010+
hash = hash.wrapping_mul(0xbf58_476d_1ce4_e5b9);
3011+
hash ^= hash >> 27;
3012+
hash = hash.wrapping_mul(0x94d0_49bb_1331_11eb);
3013+
hash ^ (hash >> 31)
3014+
}
3015+
3016+
fn batch_jobs_identical(left: (&[f32], &DecodeParams), right: (&[f32], &DecodeParams)) -> bool {
3017+
if left.1 != right.1 || left.0.len() != right.0.len() {
3018+
return false;
3019+
}
3020+
std::ptr::eq(left.0.as_ptr(), right.0.as_ptr())
3021+
|| left
3022+
.0
3023+
.iter()
3024+
.zip(right.0)
3025+
.all(|(&a, &b)| a.to_bits() == b.to_bits())
3026+
}
3027+
3028+
fn coalesce_batch_jobs(jobs: &[(&[f32], DecodeParams)]) -> Vec<BatchJobGroup> {
3029+
use std::collections::HashMap;
3030+
use std::hash::{Hash, Hasher};
3031+
3032+
let mut groups: Vec<BatchJobGroup> = Vec::with_capacity(jobs.len());
3033+
let mut by_fingerprint: HashMap<u64, Vec<usize>> = HashMap::with_capacity(jobs.len());
3034+
for (job_index, (samples, params)) in jobs.iter().enumerate() {
3035+
let mut params_hash = std::collections::hash_map::DefaultHasher::new();
3036+
params.hash(&mut params_hash);
3037+
let fingerprint = batch_audio_fingerprint(samples) ^ params_hash.finish().rotate_left(17);
3038+
let matching_group = by_fingerprint.get(&fingerprint).and_then(|candidates| {
3039+
candidates.iter().copied().find(|&group_index| {
3040+
let representative = groups[group_index].representative;
3041+
batch_jobs_identical(
3042+
(samples, params),
3043+
(jobs[representative].0, &jobs[representative].1),
3044+
)
3045+
})
3046+
});
3047+
if let Some(group_index) = matching_group {
3048+
groups[group_index].members.push(job_index);
3049+
} else {
3050+
let group_index = groups.len();
3051+
groups.push(BatchJobGroup {
3052+
representative: job_index,
3053+
members: vec![job_index],
3054+
});
3055+
by_fingerprint
3056+
.entry(fingerprint)
3057+
.or_default()
3058+
.push(group_index);
3059+
}
3060+
}
3061+
groups
3062+
}
3063+
3064+
/// Enable exact duplicate elimination in [`transcribe_samples_batch`].
3065+
///
3066+
/// The default is on. `FW_BATCH_COALESCE=0` (also `false` or `off`) restores
3067+
/// one physical transcription per input for same-binary comparison and
3068+
/// operational rollback.
3069+
fn batch_coalesce_enabled() -> bool {
3070+
use std::sync::OnceLock;
3071+
static ON: OnceLock<bool> = OnceLock::new();
3072+
*ON.get_or_init(|| {
3073+
std::env::var("FW_BATCH_COALESCE").map_or(true, |value| {
3074+
!matches!(
3075+
value.trim().to_ascii_lowercase().as_str(),
3076+
"0" | "false" | "off"
3077+
)
3078+
})
3079+
})
3080+
}
3081+
29933082
/// Transcribe independent audio inputs concurrently through one loaded model.
29943083
///
29953084
/// This is the multi-file counterpart to [`transcribe_samples`]. Model weights
@@ -2999,6 +3088,11 @@ pub fn transcribe_samples(
29993088
/// caller otherwise faces: serializing every file, or loading one multi-GB
30003089
/// model per process.
30013090
///
3091+
/// Before scheduling, jobs with bit-identical samples and equal
3092+
/// [`DecodeParams`] are coalesced. Fingerprint matches are collision-checked,
3093+
/// so distinct jobs can never alias; successful results are cloned back into
3094+
/// their original positions. Set `FW_BATCH_COALESCE=0` to disable this path.
3095+
///
30023096
/// At most one file lane is admitted per four Rayon workers. Nested frontends
30033097
/// and transformer kernels reuse that same fixed pool, so additional lanes do
30043098
/// not create additional threads; they expose independent ready work whenever
@@ -3026,25 +3120,80 @@ pub fn transcribe_samples_batch(
30263120
}
30273121

30283122
super::ensure_default_rayon_pool();
3123+
let groups = if batch_coalesce_enabled() {
3124+
coalesce_batch_jobs(jobs)
3125+
} else {
3126+
(0..jobs.len())
3127+
.map(|job_index| BatchJobGroup {
3128+
representative: job_index,
3129+
members: vec![job_index],
3130+
})
3131+
.collect()
3132+
};
3133+
super::perf_span(
3134+
"batch_coalesce",
3135+
0.0,
3136+
&format!("\"jobs\":{},\"unique\":{}", jobs.len(), groups.len()),
3137+
);
30293138
const MIN_THREADS_PER_FILE: usize = 4;
3030-
let lanes = jobs
3139+
let lanes = groups
30313140
.len()
30323141
.min((rayon::current_num_threads() / MIN_THREADS_PER_FILE).max(1));
3033-
let next_job = std::sync::atomic::AtomicUsize::new(0);
3142+
let next_group = std::sync::atomic::AtomicUsize::new(0);
30343143

30353144
let mut completed: Vec<(usize, FwResult<DecodeOutput>)> = (0..lanes)
30363145
.into_par_iter()
30373146
.flat_map_iter(|_| {
30383147
std::iter::from_fn(|| {
3039-
let index = next_job.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3040-
jobs.get(index).map(|(samples, params)| {
3041-
(index, transcribe_samples(m, samples, params, checkpoint))
3148+
let group_index = next_group.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3149+
groups.get(group_index).map(|group| {
3150+
let representative = group.representative;
3151+
let (samples, params) = &jobs[representative];
3152+
(
3153+
group_index,
3154+
transcribe_samples(m, samples, params, checkpoint),
3155+
)
30423156
})
30433157
})
30443158
})
30453159
.collect();
3046-
completed.sort_unstable_by_key(|(index, _)| *index);
3047-
completed.into_iter().map(|(_, result)| result).collect()
3160+
completed.sort_unstable_by_key(|(group_index, _)| *group_index);
3161+
3162+
let mut outputs: Vec<Option<FwResult<DecodeOutput>>> = (0..jobs.len()).map(|_| None).collect();
3163+
for (group_index, result) in completed {
3164+
let group = &groups[group_index];
3165+
match result {
3166+
Ok(output) => {
3167+
for (position, &job_index) in group.members.iter().enumerate() {
3168+
let mut logical_output = output.clone();
3169+
if position > 0 {
3170+
// Work provenance is physical, not logical: cached
3171+
// followers perform no encoder or decoder operations.
3172+
logical_output.work = DecodeWorkStats::default();
3173+
}
3174+
outputs[job_index] = Some(Ok(logical_output));
3175+
}
3176+
}
3177+
Err(error) => {
3178+
let (&first, followers) = group.members.split_first().expect("nonempty group");
3179+
outputs[first] = Some(Err(error));
3180+
// Error values are intentionally not cloneable. Re-run only
3181+
// failed followers so each receives its own precise error.
3182+
for &job_index in followers {
3183+
outputs[job_index] = Some(transcribe_samples(
3184+
m,
3185+
jobs[job_index].0,
3186+
&jobs[job_index].1,
3187+
checkpoint,
3188+
));
3189+
}
3190+
}
3191+
}
3192+
}
3193+
outputs
3194+
.into_iter()
3195+
.map(|output| output.expect("every batch group resolves its members"))
3196+
.collect()
30483197
}
30493198

30503199
/// Compute DTW word timings for one window, returning per-segment word lists
@@ -3435,6 +3584,49 @@ mod tests {
34353584
// Synthetic tokenizer for hermetic logit-filter / segment tests.
34363585
// -----------------------------------------------------------------------
34373586

3587+
#[test]
3588+
fn batch_coalescing_shares_only_exact_audio_and_params() {
3589+
let audio = vec![0.0, 0.25, -0.5, f32::from_bits(0x7fc0_0001)];
3590+
let audio_copy = audio.clone();
3591+
let signed_zero_variant = vec![-0.0, 0.25, -0.5, f32::from_bits(0x7fc0_0001)];
3592+
let params = DecodeParams::default();
3593+
let mut translated = params.clone();
3594+
translated.translate = true;
3595+
let jobs = [
3596+
(audio.as_slice(), params.clone()),
3597+
(audio_copy.as_slice(), params.clone()),
3598+
(signed_zero_variant.as_slice(), params.clone()),
3599+
(audio.as_slice(), translated),
3600+
];
3601+
3602+
let groups = coalesce_batch_jobs(&jobs);
3603+
3604+
assert_eq!(groups.len(), 3);
3605+
assert_eq!(groups[0].representative, 0);
3606+
assert_eq!(groups[0].members, vec![0, 1]);
3607+
assert_eq!(groups[1].members, vec![2]);
3608+
assert_eq!(groups[2].members, vec![3]);
3609+
}
3610+
3611+
#[test]
3612+
fn batch_coalescing_preserves_first_seen_group_order() {
3613+
let first = vec![1.0, 2.0];
3614+
let second = vec![3.0, 4.0];
3615+
let first_copy = first.clone();
3616+
let params = DecodeParams::default();
3617+
let jobs = [
3618+
(first.as_slice(), params.clone()),
3619+
(second.as_slice(), params.clone()),
3620+
(first_copy.as_slice(), params),
3621+
];
3622+
3623+
let groups = coalesce_batch_jobs(&jobs);
3624+
3625+
assert_eq!(groups.len(), 2);
3626+
assert_eq!(groups[0].members, vec![0, 2]);
3627+
assert_eq!(groups[1].members, vec![1]);
3628+
}
3629+
34383630
fn hp(n_vocab: i32) -> WhisperHParams {
34393631
WhisperHParams {
34403632
n_vocab,

0 commit comments

Comments
 (0)