Skip to content

Commit 2a6286e

Browse files
committed
Merge remote-tracking branch 'e/main'
2 parents eead2c2 + ccfbd61 commit 2a6286e

3 files changed

Lines changed: 171 additions & 35 deletions

File tree

apvd-cli/src/main.rs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,11 @@ fn run_train(
792792
};
793793

794794
let train_result = if robust {
795-
model.train_robust(max_steps)
795+
model.train_robust_with_config(
796+
apvd_core::optimization::robust::OptimConfig::default(),
797+
max_steps,
798+
tiered_config.as_ref(),
799+
)
796800
} else {
797801
model.train(learning_rate, max_steps)
798802
};
@@ -812,36 +816,42 @@ fn run_train(
812816
.map(|s| serde_json::to_value(s.v()).unwrap())
813817
.collect();
814818

815-
// Collect BTD (Best To Date) step indices
819+
// Collect BTD (Best To Date) step indices (using original indices if filtered)
816820
let mut btd_steps = Vec::new();
817821
let mut min_so_far = f64::INFINITY;
818-
for (idx, step) in model.steps.iter().enumerate() {
822+
for (i, step) in model.steps.iter().enumerate() {
819823
let error = step.error.v();
820824
if error < min_so_far {
821825
min_so_far = error;
822-
btd_steps.push(idx);
826+
let abs_idx = model.step_indices.as_ref()
827+
.map_or(i, |indices| indices[i]);
828+
btd_steps.push(abs_idx);
823829
}
824830
}
825831

826832
// Build history if requested
833+
let already_filtered = robust && tiered_config.is_some();
827834
let history = if include_history || include_checkpoints || tiered_config.is_some() {
828835
// Determine which steps to include
829-
let include_step: Box<dyn Fn(usize) -> bool> = if include_history {
830-
Box::new(|_| true) // all steps
836+
let include_step: Box<dyn Fn(usize) -> bool> = if already_filtered || include_history {
837+
// When robust + tiered, steps are already filtered during training;
838+
// when include_history, output everything.
839+
Box::new(|_| true)
831840
} else if include_checkpoints {
832841
// Checkpoint steps: 0, 100, 500, 1000, best, final
842+
let total = model.total_steps;
833843
let mut indices: std::collections::HashSet<usize> = [0, 100, 500, 1000]
834844
.iter()
835-
.filter(|&&i| i < model.steps.len())
845+
.filter(|&&i| i < total)
836846
.copied()
837847
.collect();
838848
indices.insert(model.min_idx); // best step
839-
indices.insert(model.steps.len() - 1); // final step
849+
indices.insert(total - 1); // final step
840850
Box::new(move |idx| indices.contains(&idx))
841851
} else if let Some(ref tc) = tiered_config {
842-
// Tiered keyframes
852+
// Tiered keyframes (non-robust path)
843853
let tc = tc.clone();
844-
let final_idx = model.steps.len() - 1;
854+
let final_idx = model.total_steps - 1;
845855
let min_idx = model.min_idx;
846856
Box::new(move |idx| {
847857
tc.is_keyframe(idx) || idx == final_idx || idx == min_idx
@@ -853,8 +863,16 @@ fn run_train(
853863
let mut min_so_far = f64::INFINITY;
854864
Some(
855865
model.steps.iter().enumerate()
856-
.filter(|(idx, _)| include_step(*idx))
857-
.map(|(idx, step)| {
866+
.filter_map(|(i, step)| {
867+
let abs_idx = model.step_indices.as_ref()
868+
.map_or(i, |indices| indices[i]);
869+
if include_step(abs_idx) {
870+
Some((abs_idx, step))
871+
} else {
872+
None
873+
}
874+
})
875+
.map(|(abs_idx, step)| {
858876
let error = step.error.v();
859877
let is_best = if error < min_so_far {
860878
min_so_far = error;
@@ -863,7 +881,7 @@ fn run_train(
863881
None
864882
};
865883
TraceStep {
866-
step_idx: idx,
884+
step_idx: abs_idx,
867885
error,
868886
shapes: step.shapes.iter().map(|s| serde_json::to_value(s.v()).unwrap()).collect(),
869887
is_best,
@@ -880,7 +898,7 @@ fn run_train(
880898
final_error: final_step.error.v(),
881899
min_error: model.min_error,
882900
min_step: model.min_idx,
883-
total_steps: model.steps.len(),
901+
total_steps: model.total_steps,
884902
training_time_ms,
885903
final_shapes,
886904
history,

apvd-core/src/optimization/model.rs

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ use serde::{Deserialize, Serialize};
33
use tsify::Tsify;
44

55
use crate::{error::SceneError, step::Step, targets::TargetsMap, shape::InputSpec};
6+
use crate::tiered::TieredConfig;
67
use super::adam::{AdamState, AdamConfig};
78
use super::robust::{self, OptimConfig};
89

910
#[derive(Debug, Clone, Tsify, Serialize, Deserialize)]
1011
pub struct Model {
1112
pub steps: Vec<Step>,
13+
/// Original step indices when steps are filtered (e.g. tiered keyframes).
14+
/// `None` means steps[i] corresponds to step i (no filtering).
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub step_indices: Option<Vec<usize>>,
1217
pub repeat_idx: Option<usize>,
1318
pub min_idx: usize,
1419
pub min_error: f64,
20+
/// Actual number of training iterations (may differ from `steps.len()`
21+
/// when step filtering is active).
22+
pub total_steps: usize,
1523
}
1624

1725
impl Model {
@@ -20,7 +28,7 @@ impl Model {
2028
let min_error = step.error.re;
2129
let steps = vec![step];
2230
let repeat_idx: Option<usize> = None;
23-
Ok(Model { steps, min_idx: 0, repeat_idx, min_error })
31+
Ok(Model { steps, step_indices: None, min_idx: 0, repeat_idx, min_error, total_steps: 1 })
2432
}
2533
pub fn train(&mut self, max_step_error_ratio: f64, max_steps: usize) -> Result<(), SceneError> {
2634
let num_steps = self.steps.len();
@@ -66,6 +74,7 @@ impl Model {
6674
}
6775
step = nxt;
6876
}
77+
self.total_steps = self.steps.len();
6978
Ok(())
7079
}
7180
pub fn grad_size(&self) -> usize {
@@ -128,6 +137,7 @@ impl Model {
128137
}
129138
step = nxt;
130139
}
140+
self.total_steps = self.steps.len();
131141
Ok(())
132142
}
133143

@@ -139,27 +149,49 @@ impl Model {
139149
/// - Learning rate warmup for stability
140150
/// - Step rejection when error increases significantly
141151
pub fn train_robust(&mut self, max_steps: usize) -> Result<(), SceneError> {
142-
self.train_robust_with_config(OptimConfig::default(), max_steps)
152+
self.train_robust_with_config(OptimConfig::default(), max_steps, None)
143153
}
144154

145155
/// Train using robust optimization with custom configuration.
146-
pub fn train_robust_with_config(&mut self, config: OptimConfig, max_steps: usize) -> Result<(), SceneError> {
147-
let num_steps = self.steps.len();
148-
let initial_step = &self.steps[num_steps - 1];
149-
150-
let new_steps = robust::train_robust(initial_step, config, max_steps)?;
151-
152-
// Add new steps to history, tracking min error
153-
for (idx, step) in new_steps.into_iter().skip(1).enumerate() {
154-
let step_idx = num_steps + idx;
155-
let err = step.error.re;
156+
///
157+
/// When `tiered` is `Some`, only tiered keyframe steps (plus best and final)
158+
/// are retained in memory, preventing OOM on long training runs.
159+
pub fn train_robust_with_config(
160+
&mut self,
161+
config: OptimConfig,
162+
max_steps: usize,
163+
tiered: Option<&TieredConfig>,
164+
) -> Result<(), SceneError> {
165+
let step_offset = self.steps.len() - 1;
166+
let initial_step = &self.steps[step_offset];
167+
168+
let retain: Option<Box<dyn Fn(usize) -> bool>> = tiered.map(|tc| {
169+
let tc = tc.clone();
170+
Box::new(move |idx: usize| tc.is_keyframe(idx)) as Box<dyn Fn(usize) -> bool>
171+
});
172+
173+
let result = robust::train_robust_filtered(
174+
initial_step,
175+
config,
176+
max_steps,
177+
step_offset,
178+
retain.as_deref(),
179+
)?;
180+
181+
// result.steps[0] is the initial step (already in self.steps), skip it
182+
for step in result.steps.into_iter().skip(1) {
183+
self.steps.push(step);
184+
}
156185

157-
if err < self.min_error {
158-
self.min_idx = step_idx;
159-
self.min_error = err;
160-
}
186+
self.min_idx = result.min_idx;
187+
self.min_error = result.min_error;
188+
self.total_steps = result.total_steps;
161189

162-
self.steps.push(step);
190+
// Store step indices when filtering is active
191+
if tiered.is_some() {
192+
let mut indices = vec![0usize]; // initial step
193+
indices.extend(result.step_indices.iter().skip(1));
194+
self.step_indices = Some(indices);
163195
}
164196

165197
Ok(())

apvd-core/src/optimization/robust.rs

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,56 @@ impl Optimizer {
181181
}
182182
}
183183

184-
/// Train a model using robust optimization.
184+
/// Result of robust training with optional step filtering.
185+
pub struct RobustResult {
186+
/// Retained steps (filtered by predicate + best + final).
187+
pub steps: Vec<Step>,
188+
/// Original (absolute) step index for each retained step.
189+
pub step_indices: Vec<usize>,
190+
/// Total iterations actually completed (accepted steps only).
191+
pub total_steps: usize,
192+
/// Absolute index of the best (min error) step.
193+
pub min_idx: usize,
194+
/// Best error achieved.
195+
pub min_error: f64,
196+
}
197+
198+
/// Train using robust optimization, retaining all steps.
185199
pub fn train_robust(
186200
initial_step: &Step,
187201
config: OptimConfig,
188202
max_steps: usize,
189-
) -> Result<Vec<Step>, SceneError> {
203+
) -> Result<RobustResult, SceneError> {
204+
train_robust_filtered(initial_step, config, max_steps, 0, None)
205+
}
206+
207+
/// Train using robust optimization with optional step filtering.
208+
///
209+
/// `step_offset`: added to internal indices so `retain` sees absolute step numbers.
210+
/// `retain`: if `Some`, only steps where `retain(abs_idx)` returns true are kept
211+
/// in the result (plus the best and final steps, which are always retained).
212+
pub fn train_robust_filtered(
213+
initial_step: &Step,
214+
config: OptimConfig,
215+
max_steps: usize,
216+
step_offset: usize,
217+
retain: Option<&dyn Fn(usize) -> bool>,
218+
) -> Result<RobustResult, SceneError> {
190219
let grad_size = initial_step.grad_size();
191220
let mut optimizer = Optimizer::new(grad_size, config);
221+
222+
// Always retain the initial step
192223
let mut steps = vec![initial_step.clone()];
224+
let mut step_indices = vec![step_offset];
225+
193226
let mut current = initial_step.clone();
227+
let mut min_idx = step_offset;
228+
let mut min_error = initial_step.error.v();
229+
// Track best step separately so we can ensure it's in the output
230+
let mut best_step: Option<Step> = None;
231+
232+
// Count accepted steps (excluding initial)
233+
let mut accepted_count: usize = 0;
194234

195235
for step_idx in 0..max_steps {
196236
let error = current.error.clone();
@@ -235,11 +275,29 @@ pub fn train_robust(
235275
}
236276

237277
optimizer.accept_step();
278+
accepted_count += 1;
279+
let abs_idx = step_offset + accepted_count;
238280

239281
debug!("Step {}: error {:.6} -> {:.6} (lr={:.4})",
240282
step_idx, current_error, new_error, optimizer.effective_lr());
241283

242-
steps.push(new_step.clone());
284+
// Track best
285+
if new_error < min_error {
286+
min_error = new_error;
287+
min_idx = abs_idx;
288+
best_step = Some(new_step.clone());
289+
}
290+
291+
// Decide whether to retain this step
292+
let should_retain = match retain {
293+
Some(f) => f(abs_idx),
294+
None => true,
295+
};
296+
if should_retain {
297+
steps.push(new_step.clone());
298+
step_indices.push(abs_idx);
299+
}
300+
243301
current = new_step;
244302

245303
// Check for convergence (error + penalties very small)
@@ -249,7 +307,35 @@ pub fn train_robust(
249307
}
250308
}
251309

252-
Ok(steps)
310+
let total_steps = step_offset + accepted_count + 1; // +1 for initial step
311+
312+
// Ensure best step is in the output
313+
if let Some(best) = best_step {
314+
if !step_indices.contains(&min_idx) {
315+
steps.push(best);
316+
step_indices.push(min_idx);
317+
}
318+
}
319+
320+
// Ensure final step is in the output
321+
let final_abs_idx = step_offset + accepted_count;
322+
if accepted_count > 0 && !step_indices.contains(&final_abs_idx) {
323+
steps.push(current);
324+
step_indices.push(final_abs_idx);
325+
}
326+
327+
// Sort by step index to maintain order
328+
let mut indexed: Vec<_> = step_indices.into_iter().zip(steps.into_iter()).collect();
329+
indexed.sort_by_key(|(idx, _)| *idx);
330+
let (step_indices, steps): (Vec<_>, Vec<_>) = indexed.into_iter().unzip();
331+
332+
Ok(RobustResult {
333+
steps,
334+
step_indices,
335+
total_steps,
336+
min_idx,
337+
min_error,
338+
})
253339
}
254340

255341
#[cfg(test)]

0 commit comments

Comments
 (0)