1use std::cell::RefCell;
24use std::fmt::{self, Display};
25use std::rc::Rc;
26
27use async_trait::async_trait;
28use gwr_engine::engine::Engine;
29use gwr_engine::executor::Spawner;
30use gwr_engine::port::PortStateResult;
31use gwr_engine::time::clock::{Clock, phase};
32use gwr_engine::traits::Runnable;
33use gwr_engine::types::{AccessType, SimError, SimResult};
34use gwr_model_builder::{EntityDisplay, EntityGet};
35use gwr_track::debug;
36use gwr_track::entity::{Entity, EntityGroup, EntityLane};
37use gwr_track::tracker::aka::Aka;
38use serde::{Deserialize, Serialize};
39
40use crate::log_stats;
41use crate::memory::memory_access::MemoryAccess;
42use crate::memory::memory_map::{DeviceId, MemoryMap};
43use crate::processing_element::dispatch::Dispatch;
44use crate::processing_element::flop_monitor::FlopMonitor;
45use crate::processing_element::load_store_unit::LoadStoreUnit;
46use crate::processing_element::operators::TensorView;
47use crate::processing_element::task::{ComputeTaskConfig, Task};
48
49pub mod dispatch;
50mod flop_monitor;
51mod load_store_unit;
52pub mod operators;
53pub mod task;
54
55#[derive(Clone, Copy, Eq, Hash, PartialEq)]
56pub enum MachineOp {
57 Add,
58 Compare,
59 Mul,
60}
61
62#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
63#[serde(deny_unknown_fields)]
64pub struct MachineOpCounts {
65 #[serde(default)]
66 pub adds: usize,
67 #[serde(default)]
68 pub compares: usize,
69 #[serde(default)]
70 pub muls: usize,
71}
72
73impl MachineOpCounts {
74 #[must_use]
75 pub fn total(&self) -> usize {
76 self.adds + self.compares + self.muls
77 }
78
79 pub fn add_assign(&mut self, other: Self) {
80 self.adds += other.adds;
81 self.compares += other.compares;
82 self.muls += other.muls;
83 }
84}
85
86pub struct ProcessingElementStatsDisplay {
87 prefix: String,
88 time_now_ns: f64,
89 machine_ops: MachineOpCounts,
90}
91
92impl ProcessingElementStatsDisplay {
93 #[must_use]
94 pub fn new(prefix: impl Into<String>, time_now_ns: f64, machine_ops: MachineOpCounts) -> Self {
95 Self {
96 prefix: prefix.into(),
97 time_now_ns,
98 machine_ops,
99 }
100 }
101}
102
103impl Display for ProcessingElementStatsDisplay {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 let total_flops = self.machine_ops.total();
106 let time_now_s = self.time_now_ns / 1e9;
107 let total_gflops = total_flops as f64 / 1e9;
108 let average_gflops_per_second = if time_now_s == 0.0 {
109 0.0
110 } else {
111 total_gflops / time_now_s
112 };
113
114 writeln!(f, "{}:", self.prefix)?;
115 writeln!(
116 f,
117 " FLOPs: {total_flops}, {total_gflops:.2} GFLOPs, {average_gflops_per_second:.2} GFLOP/s"
118 )?;
119 write!(
120 f,
121 " Machine ops: {} total, {} add, {} mul, {} compare",
122 self.machine_ops.total(),
123 self.machine_ops.adds,
124 self.machine_ops.muls,
125 self.machine_ops.compares
126 )
127 }
128}
129
130type Dispatcher = Rc<dyn Dispatch>;
131
132pub struct ProcessingElementConfig {
133 pub num_active_requests: usize,
135
136 pub lsu_access_bytes: usize,
138
139 pub overhead_size_bytes: usize,
141
142 pub sram_bytes: usize,
144
145 pub adds_per_tick: f64,
147
148 pub muls_per_tick: f64,
150
151 pub compares_per_tick: f64,
153}
154
155pub struct ComputeCapabilities {
156 adds_per_tick: f64,
157 muls_per_tick: f64,
158 compares_per_tick: f64,
159 sram_bytes: usize,
160}
161
162impl ComputeCapabilities {
163 #[must_use]
164 pub fn ops_per_tick(&self, op: MachineOp) -> f64 {
165 match op {
166 MachineOp::Add => self.adds_per_tick,
167 MachineOp::Compare => self.compares_per_tick,
168 MachineOp::Mul => self.muls_per_tick,
169 }
170 }
171
172 pub fn cycles_for_ops(&self, num_ops: usize, op: MachineOp) -> Result<usize, SimError> {
173 if num_ops == 0 {
174 return Ok(0);
175 }
176
177 let ops_per_tick = self.ops_per_tick(op);
178 if !ops_per_tick.is_finite() || ops_per_tick <= 0.0 {
179 return Err(SimError(format!(
180 "invalid compute throughput {ops_per_tick} ops/tick"
181 )));
182 }
183
184 Ok(((num_ops as f64) / ops_per_tick).ceil() as usize)
185 }
186}
187
188#[derive(Default)]
189struct ProcessingElementStats {
190 machine_ops: MachineOpCounts,
191}
192
193struct Lane {
194 lane: EntityLane,
195 active: bool,
196}
197
198pub(crate) struct ActivityLanes {
199 entity: Rc<Entity>,
200 track_name: String,
201 lanes: Vec<Lane>,
202}
203
204impl ActivityLanes {
205 fn new(entity: Rc<Entity>, track_name: &str) -> Self {
206 Self {
207 entity,
208 track_name: track_name.to_string(),
209 lanes: Vec::new(),
210 }
211 }
212
213 fn begin_in_group(
214 lanes: &Rc<RefCell<Self>>,
215 name: &str,
216 group: &Rc<EntityGroup>,
217 ) -> ActivityLaneGuard {
218 let mut lanes_ref = lanes.borrow_mut();
219 let lane_idx = match lanes_ref.lanes.iter().position(|lane| !lane.active) {
220 Some(lane_idx) => lane_idx,
221 None => lanes_ref.add_new_lane(),
222 };
223
224 let lane = &mut lanes_ref.lanes[lane_idx];
225 lane.lane.begin_in_group(name, group);
226 lane.active = true;
227
228 ActivityLaneGuard {
229 lanes: lanes.clone(),
230 lane_idx,
231 active: true,
232 }
233 }
234
235 fn add_new_lane(&mut self) -> usize {
236 let lane_idx = self.lanes.len();
237 let lane = EntityLane::new(&self.entity, &format!("{}::{lane_idx}", self.track_name));
238 self.lanes.push(Lane {
239 lane,
240 active: false,
241 });
242 lane_idx
243 }
244
245 fn end(&mut self, lane_idx: usize) {
246 let lane = &mut self.lanes[lane_idx];
247 lane.lane.end();
248 lane.active = false;
249 }
250}
251
252struct ActivityLaneGuard {
253 lanes: Rc<RefCell<ActivityLanes>>,
254 lane_idx: usize,
255 active: bool,
256}
257
258impl Drop for ActivityLaneGuard {
259 fn drop(&mut self) {
260 if self.active {
261 self.lanes.borrow_mut().end(self.lane_idx);
262 self.active = false;
263 }
264 }
265}
266
267struct ProcessingElementActivityLanes {
268 entity: Rc<Entity>,
269 compute: Rc<RefCell<ActivityLanes>>,
270 lsu_read: Rc<RefCell<ActivityLanes>>,
271 lsu_write: Rc<RefCell<ActivityLanes>>,
272}
273
274impl ProcessingElementActivityLanes {
275 fn new(entity: Rc<Entity>) -> Self {
276 Self {
277 entity: entity.clone(),
278 compute: Rc::new(RefCell::new(ActivityLanes::new(
279 entity.clone(),
280 "lane::compute",
281 ))),
282 lsu_read: Rc::new(RefCell::new(ActivityLanes::new(
283 entity.clone(),
284 "lane::lsu_read",
285 ))),
286 lsu_write: Rc::new(RefCell::new(ActivityLanes::new(entity, "lane::lsu_write"))),
287 }
288 }
289
290 fn create_group(&self, name: &str) -> Rc<EntityGroup> {
291 Rc::new(EntityGroup::new(&self.entity, name))
292 }
293}
294
295#[derive(EntityGet, EntityDisplay)]
296pub struct ProcessingElement {
297 entity: Rc<Entity>,
298 lsu: Rc<LoadStoreUnit>,
299 clock: Clock,
300 spawner: Spawner,
301
302 compute_capabilities: Rc<ComputeCapabilities>,
303 stats: Rc<RefCell<ProcessingElementStats>>,
304 activity_lanes: Rc<ProcessingElementActivityLanes>,
305 dispatcher: RefCell<Option<Dispatcher>>,
306 flop_monitor: Option<Rc<FlopMonitor>>,
307}
308
309impl ProcessingElement {
310 #[expect(clippy::too_many_arguments)]
311 pub fn new_and_register_with_renames(
312 engine: &Engine,
313 clock: &Clock,
314 parent: &Rc<Entity>,
315 name: &str,
316 aka: Option<&Aka>,
317 memory_map: &Rc<MemoryMap>,
318 pe_config: &ProcessingElementConfig,
319 device_id: DeviceId,
320 ) -> Result<Rc<Self>, SimError> {
321 let entity = Rc::new(Entity::new(parent, name));
322
323 let lsu = LoadStoreUnit::new_and_register(
324 engine, clock, &entity, aka, pe_config, memory_map, device_id,
325 )?;
326 let monitor_window_size = entity.tracker.monitoring_window_size_for(entity.id);
327 let flop_monitor = monitor_window_size.map(|window_size_ticks| {
328 FlopMonitor::new_and_register(engine, &entity, clock, window_size_ticks)
329 });
330
331 let rc_self = Rc::new(Self {
332 entity: entity.clone(),
333 lsu,
334 clock: clock.clone(),
335 spawner: engine.spawner(),
336
337 compute_capabilities: Rc::new(ComputeCapabilities {
338 adds_per_tick: pe_config.adds_per_tick,
339 muls_per_tick: pe_config.muls_per_tick,
340 compares_per_tick: pe_config.compares_per_tick,
341 sram_bytes: pe_config.sram_bytes,
342 }),
343 stats: Rc::new(RefCell::new(ProcessingElementStats::default())),
344 activity_lanes: Rc::new(ProcessingElementActivityLanes::new(entity.clone())),
345
346 dispatcher: RefCell::new(None),
347 flop_monitor,
348 });
349 engine.register(rc_self.clone());
350 Ok(rc_self)
351 }
352
353 pub fn new_and_register(
354 engine: &Engine,
355 clock: &Clock,
356 parent: &Rc<Entity>,
357 name: &str,
358 memory_map: &Rc<MemoryMap>,
359 pe_config: &ProcessingElementConfig,
360 device_id: DeviceId,
361 ) -> Result<Rc<Self>, SimError> {
362 Self::new_and_register_with_renames(
363 engine, clock, parent, name, None, memory_map, pe_config, device_id,
364 )
365 }
366
367 pub fn set_dispatcher(&self, dispatcher: &Dispatcher) {
368 *self.dispatcher.borrow_mut() = Some(dispatcher.clone());
369 }
370
371 pub fn connect_port_tx(&self, port_state: PortStateResult<MemoryAccess>) -> SimResult {
372 self.lsu.connect_port_tx(port_state)
373 }
374
375 pub fn port_rx(&self) -> PortStateResult<MemoryAccess> {
376 self.lsu.port_rx()
377 }
378
379 #[must_use]
380 pub fn total_graph_nodes(&self) -> usize {
381 match self.dispatcher.borrow().as_ref() {
382 None => 0,
383 Some(dispatcher) => dispatcher.total_tasks_for_pe(self.entity.name.as_str()),
384 }
385 }
386
387 #[must_use]
388 pub fn total_flops(&self) -> usize {
389 self.stats.borrow().machine_ops.total()
390 }
391
392 #[must_use]
393 pub fn machine_ops(&self) -> MachineOpCounts {
394 self.stats.borrow().machine_ops
395 }
396
397 pub fn dump_stats(&self, time_now_ns: f64) {
398 let stats = self.stats.borrow();
399 log_stats(
400 &self.entity,
401 ProcessingElementStatsDisplay::new(
402 format!("ProcessingElement {}", self.entity.full_name()),
403 time_now_ns,
404 stats.machine_ops,
405 ),
406 );
407 }
408}
409
410#[async_trait(?Send)]
411impl Runnable for ProcessingElement {
412 async fn run(&self) -> SimResult {
413 let dispatcher = self
414 .dispatcher
415 .borrow()
416 .as_ref()
417 .ok_or_else(|| SimError("Started without dispatcher".to_string()))?
418 .clone();
419
420 let pe_name = self.entity.name.as_str();
421 let (mut complete, mut ready_node_indices) = dispatcher.ready_task_indices(pe_name)?;
422
423 loop {
424 if complete {
425 break;
426 }
427 if ready_node_indices.is_empty() {
428 dispatcher.wait_for_change().await;
430 } else {
431 for task_idx in ready_node_indices.drain(..) {
433 dispatcher.set_task_active(task_idx)?;
434
435 let clock = self.clock.clone();
436 let dispatcher = dispatcher.clone();
437 let lsu = self.lsu.clone();
438 let compute_capabilities = self.compute_capabilities.clone();
439 let stats = self.stats.clone();
440 let entity = self.entity.clone();
441 let activity_lanes = self.activity_lanes.clone();
442 let flop_monitor = self.flop_monitor.clone();
443 self.spawner.spawn(async move {
444 handle_task(
445 entity,
446 clock,
447 dispatcher,
448 lsu,
449 compute_capabilities,
450 stats,
451 activity_lanes,
452 flop_monitor,
453 task_idx,
454 )
455 .await
456 });
457 }
458 }
459
460 (complete, ready_node_indices) = dispatcher.ready_task_indices(pe_name)?;
461 }
462 debug!(self.entity ; "PE {pe_name} DONE");
463 Ok(())
464 }
465}
466
467#[expect(clippy::too_many_arguments)]
468async fn handle_task(
469 entity: Rc<Entity>,
470 clock: Clock,
471 dispatcher: Dispatcher,
472 lsu: Rc<LoadStoreUnit>,
473 compute_capabilities: Rc<ComputeCapabilities>,
474 stats: Rc<RefCell<ProcessingElementStats>>,
475 activity_lanes: Rc<ProcessingElementActivityLanes>,
476 flop_monitor: Option<Rc<FlopMonitor>>,
477 task_idx: usize,
478) -> SimResult {
479 let task = dispatcher.task_by_id(task_idx)?;
480 match task {
481 Task::ComputeTask { config } => handle_compute_task(
482 clock,
483 dispatcher,
484 lsu,
485 task_idx,
486 compute_capabilities,
487 stats,
488 activity_lanes,
489 flop_monitor,
490 &config,
491 )
492 .await
493 .map_err(|err| SimError(format!("{entity} had error on task {}:\n{err}", config.id))),
494 Task::SyncTask { .. } => {
495 todo!();
496 }
497 }
498}
499
500fn tensor_view_num_bytes(view: &TensorView) -> usize {
501 view.num_bytes()
502}
503
504fn tensor_view_base_addr(view: &TensorView) -> Result<u64, SimError> {
505 let base_addr = view.tensor().addr();
506 let element_offset = view.element_offset()?;
507 let dtype = view.tensor().dtype();
508 let byte_offset = (dtype.num_bits() * element_offset).div_ceil(8) as u64;
509 Ok(base_addr + byte_offset)
510}
511
512#[expect(clippy::too_many_arguments)]
513async fn handle_compute_task(
514 clock: Clock,
515 dispatcher: Dispatcher,
516 lsu: Rc<LoadStoreUnit>,
517 task_idx: usize,
518 compute_capabilities: Rc<ComputeCapabilities>,
519 stats: Rc<RefCell<ProcessingElementStats>>,
520 activity_lanes: Rc<ProcessingElementActivityLanes>,
521 flop_monitor: Option<Rc<FlopMonitor>>,
522 config: &ComputeTaskConfig,
523) -> SimResult {
524 let total_num_bytes: usize = config
525 .inputs
526 .iter()
527 .chain(config.outputs.iter())
528 .filter_map(|view| view.as_ref())
529 .map(tensor_view_num_bytes)
530 .sum();
531
532 let num_partitions = total_num_bytes
533 .div_ceil(compute_capabilities.sram_bytes.max(1))
534 .max(1);
535
536 let partitions =
537 config
538 .op
539 .create_partitions(&config.inputs, &config.outputs, num_partitions)?;
540 let activity_name = config.activity_name();
541 let group = activity_lanes.create_group(&format!("{activity_name} operation"));
542
543 for partition in partitions {
544 for (idx, view) in partition.inputs.iter().enumerate() {
545 let Some(view) = view else {
546 continue;
547 };
548 lsu.do_access(
549 AccessType::ReadRequest,
550 tensor_view_num_bytes(view),
551 tensor_view_base_addr(view)?,
552 &activity_lanes.lsu_read,
553 &format!("{activity_name} tensor {idx} read"),
554 &group,
555 )
556 .await?;
557 }
558
559 let compute_ticks = config.op.compute_delay_ticks(
560 &compute_capabilities,
561 &partition.inputs,
562 &partition.outputs,
563 )?;
564 let machine_ops = config
565 .op
566 .compute_machine_ops(&partition.inputs, &partition.outputs)?;
567 let compute_flops = machine_ops.total();
568 if let Some(flop_monitor) = &flop_monitor {
569 flop_monitor.record_interval(compute_ticks as u64, compute_flops as f64);
570 }
571 {
572 clock.wait_phase(phase::END).await;
577
578 let _activity = ActivityLanes::begin_in_group(
579 &activity_lanes.compute,
580 &format!("{activity_name} compute"),
581 &group,
582 );
583 clock.wait_ticks(compute_ticks as u64).await;
584 }
585 stats.borrow_mut().machine_ops.add_assign(machine_ops);
586
587 for (idx, view) in partition.outputs.iter().enumerate() {
588 let Some(view) = view else {
589 continue;
590 };
591 lsu.do_access(
592 AccessType::WriteNonPostedRequest,
593 tensor_view_num_bytes(view),
594 tensor_view_base_addr(view)?,
595 &activity_lanes.lsu_write,
596 &format!("{activity_name} tensor {idx} write"),
597 &group,
598 )
599 .await?;
600 }
601 }
602
603 dispatcher.set_task_completed(task_idx)?;
604 Ok(())
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn cycles_for_ops_uses_ceil_for_fractional_throughput() {
613 let compute_capabilities = ComputeCapabilities {
614 adds_per_tick: 0.5,
615 muls_per_tick: 2.5,
616 compares_per_tick: 4.0,
617 sram_bytes: 1024,
618 };
619
620 assert_eq!(
621 compute_capabilities
622 .cycles_for_ops(3, MachineOp::Add)
623 .unwrap(),
624 6
625 );
626 assert_eq!(
627 compute_capabilities
628 .cycles_for_ops(6, MachineOp::Mul)
629 .unwrap(),
630 3
631 );
632 assert_eq!(
633 compute_capabilities
634 .cycles_for_ops(0, MachineOp::Compare)
635 .unwrap(),
636 0
637 );
638 }
639
640 #[test]
641 fn cycles_for_ops_rejects_invalid_throughput() {
642 let compute_capabilities = ComputeCapabilities {
643 adds_per_tick: 0.0,
644 muls_per_tick: -1.0,
645 compares_per_tick: f64::INFINITY,
646 sram_bytes: 1024,
647 };
648
649 assert!(
650 compute_capabilities
651 .cycles_for_ops(1, MachineOp::Add)
652 .is_err()
653 );
654 assert!(
655 compute_capabilities
656 .cycles_for_ops(1, MachineOp::Mul)
657 .is_err()
658 );
659 assert!(
660 compute_capabilities
661 .cycles_for_ops(1, MachineOp::Compare)
662 .is_err()
663 );
664
665 let compute_capabilities = ComputeCapabilities {
666 adds_per_tick: f64::NAN,
667 muls_per_tick: 1.0,
668 compares_per_tick: 1.0,
669 sram_bytes: 1024,
670 };
671
672 assert!(
673 compute_capabilities
674 .cycles_for_ops(1, MachineOp::Add)
675 .is_err()
676 );
677 }
678}