Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 46 additions & 9 deletions crates/openjd-cli/src/run/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,23 +537,45 @@ fn adaptive_target_runtime(step: &Step, is_adaptive: bool) -> f64 {
.unwrap_or(0.0)
}

fn calculate_adaptive_chunk_size(
current_chunk_size: usize,
completed_task_count: usize,
completed_task_duration: f64,
target_runtime_seconds: f64,
) -> Option<usize> {
if completed_task_count == 0
|| completed_task_duration <= 0.0
|| !completed_task_duration.is_finite()
{
return None;
}

let duration_per_task = completed_task_duration / completed_task_count as f64;
let mut adaptive_chunk_size = target_runtime_seconds / duration_per_task;
if completed_task_count < 10 && adaptive_chunk_size > current_chunk_size as f64 {
adaptive_chunk_size = 0.75 * current_chunk_size as f64 + 0.25 * adaptive_chunk_size;
}

Some((adaptive_chunk_size as usize).max(1))
}

fn adjust_adaptive_chunk_size(
ctx: &RunContext,
iter: &mut openjd_model::StepParameterSpaceIterator,
completed_task_count: usize,
completed_task_duration: f64,
target_runtime_seconds: f64,
) {
let duration_per_task = completed_task_duration / completed_task_count as f64;
let mut adaptive_chunk_size = target_runtime_seconds / duration_per_task;
if completed_task_count < 10 {
let current = iter.chunks_default_task_count().unwrap_or(1) as f64;
if adaptive_chunk_size > current {
adaptive_chunk_size = 0.75 * current + 0.25 * adaptive_chunk_size;
}
}
let current_chunk_size = iter.chunks_default_task_count().unwrap_or(1);
let Some(adaptive_chunk_size) = calculate_adaptive_chunk_size(
current_chunk_size,
completed_task_count,
completed_task_duration,
target_runtime_seconds,
) else {
return;
};

let adaptive_chunk_size = (adaptive_chunk_size as usize).max(1);
if Some(adaptive_chunk_size) != iter.chunks_default_task_count() {
println!(
"{}\tAdjusting chunk size to {adaptive_chunk_size}",
Expand Down Expand Up @@ -603,3 +625,18 @@ fn report_result(ctx: &mut RunContext, args: &RunArgs, job: &Job, working_dir: &
};
crate::common::print_cli_result(&result, &args.output);
}

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

#[test]
fn zero_duration_defers_adaptive_chunk_adjustment() {
assert_eq!(calculate_adaptive_chunk_size(1, 1, 0.0, 60.0), None);
}

#[test]
fn measurable_duration_preserves_early_sample_blending() {
assert_eq!(calculate_adaptive_chunk_size(1, 1, 1.0, 9.0), Some(3));
}
}
22 changes: 13 additions & 9 deletions specs/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,21 @@ dynamically adjust chunk sizes toward a target runtime:

```
For each completed task:
1. Count items in the chunk (from the RangeExpr value)
2. Accumulate total items and total duration
3. Compute duration_per_task = total_duration / total_items
4. Compute ideal_chunk_size = target_runtime_seconds / duration_per_task
5. For the first 10 tasks, blend: 75% current + 25% ideal (conservative ramp)
6. Clamp to minimum of 1
7. Update iterator's default task count if changed
1. Count items in the chunk (from the RangeExpr value)
2. Accumulate total items and total duration
3. If the cumulative duration is zero or non-finite, keep the current chunk size and wait
for a measurable sample
4. Compute duration_per_task = total_duration / total_items
5. Compute ideal_chunk_size = target_runtime_seconds / duration_per_task
6. For the first 10 tasks, blend: 75% current + 25% ideal (conservative ramp)
7. Clamp to minimum of 1
8. Update iterator's default task count if changed
```

The blending in step 5 prevents wild oscillation when early tasks have atypical durations.
After 10 tasks, the estimate stabilizes and the ideal size is used directly.
Deferring adjustment when elapsed time is not measurable prevents a zero duration from
producing an infinite ideal size that would saturate to `usize::MAX`. The blending in step 6
prevents wild oscillation when early tasks have atypical durations. After 10 tasks, the
estimate stabilizes and the ideal size is used directly.

`target_runtime_seconds` comes from the step's CHUNK[INT] parameter definition. If not
specified or zero, the model layer's `StepParameterSpaceIterator` sets `chunks_adaptive()`
Expand Down
Loading