1use crate::{Optimizer, OptimizerError, OptimizerResult, OptimizerState, ParamGroupState};
8use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::Arc;
11use torsh_core::error::{Result, TorshError};
12use torsh_tensor::Tensor;
13
14pub struct AsyncSGD {
20 lr: f32,
22 momentum: Option<f32>,
24 weight_decay: Option<f32>,
26 dampening: Option<f32>,
28 nesterov: bool,
30 params: Vec<Arc<RwLock<Tensor>>>,
32 momentum_buffers: HashMap<String, Tensor>,
34 async_config: AsyncConfig,
36 staleness_tracker: StalenessTracker,
38}
39
40#[derive(Debug, Clone)]
42pub struct AsyncConfig {
43 pub max_staleness: usize,
45 pub staleness_penalty: f32,
47 pub bounded_staleness: bool,
49 pub async_frequency: usize,
51 pub parameter_mixing: bool,
53 pub mixing_ratio: f32,
55}
56
57impl Default for AsyncConfig {
58 fn default() -> Self {
59 Self {
60 max_staleness: 10,
61 staleness_penalty: 0.9,
62 bounded_staleness: true,
63 async_frequency: 1,
64 parameter_mixing: false,
65 mixing_ratio: 0.1,
66 }
67 }
68}
69
70#[derive(Debug)]
72struct StalenessTracker {
73 global_updates: u64,
75 parameter_updates: HashMap<String, u64>,
77 staleness_history: HashMap<String, Vec<u64>>,
79}
80
81impl StalenessTracker {
82 fn new() -> Self {
83 Self {
84 global_updates: 0,
85 parameter_updates: HashMap::new(),
86 staleness_history: HashMap::new(),
87 }
88 }
89
90 fn record_update(&mut self, param_id: &str) {
92 self.global_updates += 1;
93 self.parameter_updates
94 .insert(param_id.to_string(), self.global_updates);
95
96 let history = self
98 .staleness_history
99 .entry(param_id.to_string())
100 .or_insert_with(Vec::new);
101 history.push(self.global_updates);
102
103 if history.len() > 100 {
105 history.drain(0..history.len() - 100);
106 }
107 }
108
109 fn get_staleness(&self, param_id: &str) -> u64 {
111 if let Some(&last_update) = self.parameter_updates.get(param_id) {
112 self.global_updates.saturating_sub(last_update)
113 } else {
114 self.global_updates
115 }
116 }
117
118 fn get_adaptive_lr(&self, param_id: &str, base_lr: f32, config: &AsyncConfig) -> f32 {
120 let staleness = self.get_staleness(param_id);
121
122 if staleness == 0 {
123 return base_lr;
124 }
125
126 let penalty = config.staleness_penalty.powf(staleness as f32);
128 base_lr * penalty
129 }
130}
131
132impl AsyncSGD {
133 pub fn new(
135 params: Vec<Arc<RwLock<Tensor>>>,
136 lr: f32,
137 momentum: Option<f32>,
138 weight_decay: Option<f32>,
139 dampening: Option<f32>,
140 nesterov: bool,
141 async_config: Option<AsyncConfig>,
142 ) -> Self {
143 Self {
144 lr,
145 momentum,
146 weight_decay,
147 dampening,
148 nesterov,
149 params,
150 momentum_buffers: HashMap::new(),
151 async_config: async_config.unwrap_or_default(),
152 staleness_tracker: StalenessTracker::new(),
153 }
154 }
155
156 pub fn new_async(params: Vec<Arc<RwLock<Tensor>>>, lr: f32) -> Self {
158 Self::new(params, lr, None, None, None, false, None)
159 }
160
161 pub fn async_step(&mut self, param_id: &str) -> Result<()> {
163 let param_arc = self
165 .params
166 .iter()
167 .find(|p| {
168 format!("{:p}", p.as_ref()) == param_id
170 })
171 .ok_or_else(|| {
172 TorshError::InvalidArgument(format!("Parameter with ID {} not found", param_id))
173 })?;
174
175 let mut param = param_arc.write();
176 let grad = param
177 .grad()
178 .ok_or_else(|| TorshError::AutogradError("No gradient available".to_string()))?;
179
180 let adaptive_lr =
182 self.staleness_tracker
183 .get_adaptive_lr(param_id, self.lr, &self.async_config);
184
185 if self.async_config.bounded_staleness {
187 let staleness = self.staleness_tracker.get_staleness(param_id);
188 if staleness > self.async_config.max_staleness as u64 {
189 return Ok(());
191 }
192 }
193
194 let mut effective_grad = grad.clone();
196 if let Some(decay) = self.weight_decay {
197 effective_grad = effective_grad.add(¶m.mul_scalar(decay)?)?;
198 }
199
200 if let Some(momentum) = self.momentum {
202 let param_key = format!("{:p}", param_arc.as_ref());
203
204 if let Some(buf) = self.momentum_buffers.get(¶m_key) {
205 let dampening = self.dampening.unwrap_or(0.0);
206 let new_buf = buf
207 .mul_scalar(momentum)?
208 .add(&effective_grad.mul_scalar(1.0 - dampening)?)?;
209
210 effective_grad = if self.nesterov {
211 effective_grad.add(&new_buf.mul_scalar(momentum)?)?
212 } else {
213 new_buf.clone()
214 };
215
216 self.momentum_buffers.insert(param_key, new_buf);
217 } else {
218 self.momentum_buffers
219 .insert(param_key, effective_grad.clone());
220 }
221 }
222
223 let update = effective_grad.mul_scalar(adaptive_lr)?;
225 crate::param_update::sub_assign(&mut param, &update)?;
226
227 self.staleness_tracker.record_update(param_id);
229
230 Ok(())
231 }
232
233 pub fn staleness_info(&self) -> HashMap<String, u64> {
235 self.params
236 .iter()
237 .map(|p| {
238 let param_id = format!("{:p}", p.as_ref());
239 let staleness = self.staleness_tracker.get_staleness(¶m_id);
240 (param_id, staleness)
241 })
242 .collect()
243 }
244
245 pub fn async_config(&self) -> &AsyncConfig {
247 &self.async_config
248 }
249
250 pub fn set_async_config(&mut self, config: AsyncConfig) {
252 self.async_config = config;
253 }
254
255 pub fn mix_parameters(&mut self, other_params: &[Arc<RwLock<Tensor>>]) -> Result<()> {
257 if !self.async_config.parameter_mixing {
258 return Ok(());
259 }
260
261 let mixing_ratio = self.async_config.mixing_ratio;
262
263 for (param_arc, other_param_arc) in self.params.iter().zip(other_params.iter()) {
264 let mut param = param_arc.write();
265 let other_param = other_param_arc.read();
266
267 let mixed = param
269 .mul_scalar(1.0 - mixing_ratio)?
270 .add(&other_param.mul_scalar(mixing_ratio)?)?;
271
272 crate::param_update::assign(&mut param, &mixed)?;
273 }
274
275 Ok(())
276 }
277}
278
279impl Optimizer for AsyncSGD {
280 fn step(&mut self) -> OptimizerResult<()> {
281 let param_ids: Vec<String> = self
283 .params
284 .iter()
285 .map(|param_arc| format!("{:p}", param_arc.as_ref()))
286 .collect();
287 for param_id in param_ids {
288 self.async_step(¶m_id)?;
289 }
290 Ok(())
291 }
292
293 fn zero_grad(&mut self) {
294 for param in &self.params {
295 param.write().zero_grad();
296 }
297 }
298
299 fn get_lr(&self) -> Vec<f32> {
300 vec![self.lr]
301 }
302
303 fn set_lr(&mut self, lr: f32) {
304 self.lr = lr;
305 }
306
307 fn add_param_group(
308 &mut self,
309 params: Vec<Arc<RwLock<Tensor>>>,
310 _options: HashMap<String, f32>,
311 ) {
312 self.params.extend(params);
313 }
314
315 fn parameters(&self) -> Vec<Arc<RwLock<Tensor>>> {
316 self.params.clone()
317 }
318
319 fn state_dict(&self) -> OptimizerResult<OptimizerState> {
320 let param_group = ParamGroupState {
321 lr: self.lr,
322 options: [
323 ("momentum".to_string(), self.momentum.unwrap_or(0.0)),
324 ("weight_decay".to_string(), self.weight_decay.unwrap_or(0.0)),
325 ("dampening".to_string(), self.dampening.unwrap_or(0.0)),
326 (
327 "nesterov".to_string(),
328 if self.nesterov { 1.0 } else { 0.0 },
329 ),
330 ]
331 .iter()
332 .cloned()
333 .collect(),
334 param_count: self.params.len(),
335 };
336
337 let mut state = HashMap::new();
339 for (param_id, momentum_buffer) in &self.momentum_buffers {
340 let mut param_state = HashMap::new();
341 param_state.insert("momentum_buffer".to_string(), momentum_buffer.clone());
342 state.insert(param_id.clone(), param_state);
343 }
344
345 let mut global_state = HashMap::new();
347
348 global_state.insert(
350 "max_staleness".to_string(),
351 self.async_config.max_staleness as f32,
352 );
353 global_state.insert(
354 "staleness_penalty".to_string(),
355 self.async_config.staleness_penalty,
356 );
357 global_state.insert(
358 "bounded_staleness".to_string(),
359 if self.async_config.bounded_staleness {
360 1.0
361 } else {
362 0.0
363 },
364 );
365 global_state.insert(
366 "async_frequency".to_string(),
367 self.async_config.async_frequency as f32,
368 );
369 global_state.insert(
370 "parameter_mixing".to_string(),
371 if self.async_config.parameter_mixing {
372 1.0
373 } else {
374 0.0
375 },
376 );
377 global_state.insert("mixing_ratio".to_string(), self.async_config.mixing_ratio);
378
379 global_state.insert(
381 "global_updates".to_string(),
382 self.staleness_tracker.global_updates as f32,
383 );
384
385 Ok(OptimizerState {
386 optimizer_type: "AsyncSGD".to_string(),
387 version: "0.1.0".to_string(),
388 param_groups: vec![param_group],
389 state,
390 global_state,
391 })
392 }
393
394 fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
395 if state.optimizer_type != "AsyncSGD" {
397 return Err(OptimizerError::InvalidParameter(format!(
398 "Expected AsyncSGD optimizer state, got {}",
399 state.optimizer_type
400 )));
401 }
402
403 if let Some(param_group) = state.param_groups.first() {
405 self.lr = param_group.lr;
406
407 if let Some(&momentum) = param_group.options.get("momentum") {
409 self.momentum = if momentum > 0.0 { Some(momentum) } else { None };
410 }
411 if let Some(&weight_decay) = param_group.options.get("weight_decay") {
412 self.weight_decay = if weight_decay > 0.0 {
413 Some(weight_decay)
414 } else {
415 None
416 };
417 }
418 if let Some(&dampening) = param_group.options.get("dampening") {
419 self.dampening = if dampening > 0.0 {
420 Some(dampening)
421 } else {
422 None
423 };
424 }
425 if let Some(&nesterov) = param_group.options.get("nesterov") {
426 self.nesterov = nesterov > 0.0;
427 }
428
429 if param_group.param_count != self.params.len() {
431 return Err(OptimizerError::InvalidParameter(format!(
432 "Parameter count mismatch: expected {}, got {}",
433 self.params.len(),
434 param_group.param_count
435 )));
436 }
437 } else {
438 return Err(OptimizerError::InvalidParameter(
439 "No parameter groups found in state".to_string(),
440 ));
441 }
442
443 self.momentum_buffers.clear();
445 for (param_id, param_state) in state.state {
446 if let Some(momentum_buffer) = param_state.get("momentum_buffer") {
447 self.momentum_buffers
448 .insert(param_id, momentum_buffer.clone());
449 }
450 }
451
452 if let Some(&max_staleness) = state.global_state.get("max_staleness") {
454 self.async_config.max_staleness = max_staleness as usize;
455 }
456 if let Some(&staleness_penalty) = state.global_state.get("staleness_penalty") {
457 self.async_config.staleness_penalty = staleness_penalty;
458 }
459 if let Some(&bounded_staleness) = state.global_state.get("bounded_staleness") {
460 self.async_config.bounded_staleness = bounded_staleness > 0.0;
461 }
462 if let Some(&async_frequency) = state.global_state.get("async_frequency") {
463 self.async_config.async_frequency = async_frequency as usize;
464 }
465 if let Some(¶meter_mixing) = state.global_state.get("parameter_mixing") {
466 self.async_config.parameter_mixing = parameter_mixing > 0.0;
467 }
468 if let Some(&mixing_ratio) = state.global_state.get("mixing_ratio") {
469 self.async_config.mixing_ratio = mixing_ratio;
470 }
471
472 if let Some(&global_updates) = state.global_state.get("global_updates") {
474 self.staleness_tracker.global_updates = global_updates as u64;
475 }
476
477 self.staleness_tracker.parameter_updates.clear();
480 self.staleness_tracker.staleness_history.clear();
481
482 Ok(())
483 }
484}
485
486pub mod utils {
488 use super::*;
489
490 pub fn create_async_sgd_distributed(
492 params: Vec<Arc<RwLock<Tensor>>>,
493 lr: f32,
494 world_size: usize,
495 ) -> AsyncSGD {
496 let async_config = AsyncConfig {
497 max_staleness: world_size * 2, staleness_penalty: 0.9,
499 bounded_staleness: true,
500 async_frequency: 1,
501 parameter_mixing: world_size > 4, mixing_ratio: 0.1 / (world_size as f32).sqrt(), };
504
505 AsyncSGD::new(
506 params,
507 lr,
508 Some(0.9),
509 Some(1e-4),
510 None,
511 false,
512 Some(async_config),
513 )
514 }
515
516 pub fn synchronize_async_sgd_workers(workers: &mut [AsyncSGD]) -> Result<()> {
518 if workers.is_empty() {
519 return Ok(());
520 }
521
522 let num_workers = workers.len();
524
525 let param_count = workers[0].params.len();
527
528 for param_idx in 0..param_count {
530 let mut param_sum: Option<Tensor> = None;
532
533 for worker in workers.iter() {
534 if param_idx < worker.params.len() {
535 let param = worker.params[param_idx].read();
536 if let Some(ref mut sum) = param_sum {
537 *sum = sum.add(&*param)?;
538 } else {
539 param_sum = Some(param.clone());
540 }
541 }
542 }
543
544 if let Some(sum) = param_sum {
546 let average = sum.div_scalar(num_workers as f32)?;
547
548 for worker in workers.iter_mut() {
549 if param_idx < worker.params.len() {
550 let mut param = worker.params[param_idx].write();
551 crate::param_update::assign(&mut param, &average)?;
552 }
553 }
554 }
555 }
556
557 Ok(())
558 }
559
560 pub fn global_staleness_stats(workers: &[AsyncSGD]) -> HashMap<String, f32> {
562 let mut stats = HashMap::new();
563
564 if workers.is_empty() {
565 return stats;
566 }
567
568 let mut total_staleness = 0.0;
569 let mut max_staleness: f32 = 0.0;
570 let mut param_count = 0;
571
572 for worker in workers {
573 let staleness_info = worker.staleness_info();
574 for staleness in staleness_info.values() {
575 let staleness_f32 = *staleness as f32;
576 total_staleness += staleness_f32;
577 max_staleness = max_staleness.max(staleness_f32);
578 param_count += 1;
579 }
580 }
581
582 if param_count > 0 {
583 stats.insert(
584 "average_staleness".to_string(),
585 total_staleness / param_count as f32,
586 );
587 stats.insert("max_staleness".to_string(), max_staleness);
588 stats.insert("total_parameters".to_string(), param_count as f32);
589 }
590
591 stats
592 }
593}