OR-1 dataflow CPU sketch
at main 708 lines 30 kB view raw
1""" 2Frame-based ProcessingElement for OR1 dataflow CPU. 3 4Implements: 5- Frame-based matching with tag_store + presence bits 6- Mode-driven output routing (INHERIT, CHANGE_TAG, SINK) 7- PE-level EXTRACT_TAG and ALLOC_REMOTE handling 8- Side path handling for PELocalWriteToken and FrameControlToken 9- Cycle-accurate pipeline: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths 10""" 11 12import logging 13from typing import Optional 14 15import simpy 16 17from cm_inst import ( 18 ALUOp, ArithOp, FrameDest, FrameOp, FrameSlotValue, 19 Instruction, LogicOp, MemOp, OutputStyle, Port, RoutingOp, 20 TokenKind, is_monadic_alu, 21) 22from encoding import pack_flit1, unpack_flit1, unpack_instruction 23from emu.alu import execute 24from emu.events import ( 25 Emitted, EventCallback, Executed, FrameAllocated, FrameFreed, 26 FrameSlotWritten, IRAMWritten, Matched, TokenReceived, TokenRejected, 27) 28from emu.types import PEConfig 29from tokens import ( 30 CMToken, DyadToken, FrameControlToken, 31 MonadToken, PELocalWriteToken, PEToken, SMToken, Token, 32) 33 34logger = logging.getLogger(__name__) 35 36 37class ProcessingElement: 38 """Frame-based Processing Element for OR1 dataflow CPU. 39 40 Manages: 41 - Frame store: [frame_count][frame_slots] dense per-activation data 42 - Tag store: act_id → (frame_id, lane) mapping 43 - Match data: [frame_id][matchable_offsets][lane_count] for operand values 44 - Presence bits: [frame_id][matchable_offsets][lane_count] for dyadic matching 45 - Port store: [frame_id][matchable_offsets][lane_count] for port metadata 46 - Lane free: per-frame set of available lane IDs 47 - Free frames: pool of available frame IDs 48 49 Pipeline (per token): 50 - Side paths (FrameControlToken, PELocalWriteToken): 1 cycle 51 - Dyadic CMToken: 5 cycles (dequeue + IFETCH + MATCH + EXECUTE + EMIT) 52 - Monadic CMToken: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) 53 """ 54 55 def __init__( 56 self, 57 env: simpy.Environment, 58 pe_id: int, 59 config: PEConfig, 60 ): 61 self.env = env 62 self.pe_id = pe_id 63 self.frame_count = config.frame_count 64 self.frame_slots = config.frame_slots 65 self.matchable_offsets = config.matchable_offsets 66 67 # Frame storage 68 self.frames: list[list[Optional[FrameSlotValue]]] = [ 69 [None for _ in range(config.frame_slots)] 70 for _ in range(config.frame_count) 71 ] 72 73 # Tag store: act_id → (frame_id, lane) 74 self.tag_store: dict[int, tuple[int, int]] = dict(config.initial_tag_store or {}) 75 76 # Match data: [frame_id][match_slot][lane] - operand values waiting for partner 77 self.match_data: list[list[list[Optional[int]]]] = [ 78 [ 79 [None for _ in range(config.lane_count)] 80 for _ in range(config.matchable_offsets) 81 ] 82 for _ in range(config.frame_count) 83 ] 84 85 # Presence bits: [frame_id][match_slot][lane] - True if operand waiting for partner 86 self.presence: list[list[list[bool]]] = [ 87 [ 88 [False for _ in range(config.lane_count)] 89 for _ in range(config.matchable_offsets) 90 ] 91 for _ in range(config.frame_count) 92 ] 93 94 # Port store: [frame_id][match_slot][lane] - port of waiting operand 95 self.port_store: list[list[list[Optional[Port]]]] = [ 96 [ 97 [None for _ in range(config.lane_count)] 98 for _ in range(config.matchable_offsets) 99 ] 100 for _ in range(config.frame_count) 101 ] 102 103 self.lane_count = config.lane_count 104 105 # Free frames pool 106 self.free_frames = list(range(config.frame_count)) 107 for frame_id, _lane in self.tag_store.values(): 108 if frame_id in self.free_frames: 109 self.free_frames.remove(frame_id) 110 111 # Lane tracking: which lanes are free per frame 112 self.lane_free: dict[int, set[int]] = {} 113 114 # Initialize lane_free for pre-loaded tag_store entries 115 for act_id, (frame_id, lane) in self.tag_store.items(): 116 if frame_id not in self.lane_free: 117 # First time seeing this frame — set up lane tracking 118 all_lanes = set(range(self.lane_count)) 119 self.lane_free[frame_id] = all_lanes - {lane} 120 else: 121 self.lane_free[frame_id].discard(lane) 122 123 # Load initial frame data 124 if config.initial_frames: 125 for frame_id, slots in config.initial_frames.items(): 126 if isinstance(slots, dict): 127 # Dict format: {slot_idx: slot_value} 128 # Slot values can be: 129 # - int (packed flit1 from codegen) → unpack to FrameDest 130 # - FrameDest (from direct test construction) → use as-is 131 for slot_idx, slot_value in slots.items(): 132 if 0 <= slot_idx < config.frame_slots: 133 if isinstance(slot_value, int): 134 # Packed flit1 from codegen: unpack to FrameDest 135 self.frames[frame_id][slot_idx] = unpack_flit1(slot_value) 136 else: 137 # Already a FrameDest or other value: use as-is 138 self.frames[frame_id][slot_idx] = slot_value 139 elif isinstance(slots, list): 140 # List format: [slot0, slot1, ...] raw values 141 for slot_idx, slot_value in enumerate(slots): 142 if 0 <= slot_idx < config.frame_slots: 143 self.frames[frame_id][slot_idx] = slot_value 144 145 # IRAM 146 self.iram: dict[int, Instruction] = config.iram or {} 147 148 # Network routing 149 self.input_store: simpy.Store = simpy.Store(env) 150 self.route_table: dict[int, simpy.Store] = {} 151 self.sm_routes: dict[int, simpy.Store] = {} 152 153 # Observability 154 self._on_event: EventCallback = config.on_event or (lambda _: None) 155 self._component = f"pe:{pe_id}" 156 self.output_log: list = [] 157 158 # Start main process 159 self.process = env.process(self._run()) 160 161 def _run(self) -> None: 162 """Main loop: dequeue token, emit TokenReceived, spawn processor.""" 163 while True: 164 token = yield self.input_store.get() 165 yield self.env.timeout(1) # dequeue cycle 166 self._on_event(TokenReceived( 167 time=self.env.now, component=self._component, token=token, 168 )) 169 self.env.process(self._process_token(token)) 170 171 def _process_token(self, token: PEToken) -> None: 172 """Process a single token through the pipeline. 173 174 Dispatches to side paths (FrameControlToken, PELocalWriteToken) or 175 CMToken pipeline (IFETCH → act_id resolution → MATCH → EXECUTE → EMIT). 176 """ 177 if isinstance(token, FrameControlToken): 178 yield self.env.timeout(1) 179 self._handle_frame_control(token) 180 return 181 182 if isinstance(token, PELocalWriteToken): 183 yield self.env.timeout(1) 184 self._handle_local_write(token) 185 return 186 187 # CMToken pipeline: IFETCH → act_id resolution → MATCH → EXECUTE → EMIT 188 if not isinstance(token, CMToken): 189 logger.warning(f"PE {self.pe_id}: unknown token type {type(token)}") 190 return 191 192 # IFETCH (1 cycle) 193 inst = self.iram.get(token.offset) 194 yield self.env.timeout(1) 195 if inst is None: 196 logger.warning(f"PE {self.pe_id}: no instruction at offset {token.offset}") 197 return 198 199 # Act_id resolution (no cycle - just validation) 200 if token.act_id not in self.tag_store: 201 self._on_event(TokenRejected( 202 time=self.env.now, component=self._component, 203 token=token, reason=f"act_id {token.act_id} not in tag store", 204 )) 205 return 206 207 frame_id, lane = self.tag_store[token.act_id] 208 209 # Determine if monadic or dyadic instruction 210 is_monadic = ( 211 isinstance(token, MonadToken) or 212 (isinstance(token, DyadToken) and ( 213 isinstance(inst.opcode, MemOp) or 214 (isinstance(inst.opcode, ALUOp) and is_monadic_alu(inst.opcode)) 215 )) 216 ) 217 218 # MATCH (1 cycle for dyadic, 0 for monadic) 219 if isinstance(token, MonadToken): 220 left, right = token.data, None 221 elif isinstance(token, DyadToken): 222 if is_monadic: 223 left, right = token.data, None 224 else: 225 # Dyadic matching via presence bits 226 operands = self._match_frame(token, inst, frame_id, lane) 227 yield self.env.timeout(1) # match cycle 228 if operands is None: 229 return # waiting for partner 230 left, right = operands 231 else: 232 return 233 234 # EXECUTE & EMIT depends on opcode type 235 if isinstance(inst.opcode, MemOp): 236 # SM dispatch: EXECUTE cycle computes, then EMIT cycle delivers 237 # Total: 4 cycles for monadic (dequeue + IFETCH + EXECUTE + EMIT) 238 self._on_event(Executed( 239 time=self.env.now, component=self._component, 240 op=inst.opcode, result=0, bool_out=False, 241 )) 242 yield self.env.timeout(1) # EXECUTE cycle 243 yield self.env.timeout(1) # EMIT cycle 244 self._build_and_emit_sm_new(inst, left, right, token.act_id, frame_id) 245 elif inst.opcode == RoutingOp.EXTRACT_TAG: 246 # PE-level: pack current PE/act_id/offset into flit 1 247 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) 248 result = pack_flit1(FrameDest( 249 target_pe=self.pe_id, 250 offset=token.offset, 251 act_id=token.act_id, 252 port=Port.L, 253 token_kind=TokenKind.DYADIC, 254 )) 255 self._on_event(Executed( 256 time=self.env.now, component=self._component, 257 op=inst.opcode, result=result, bool_out=False, 258 )) 259 yield self.env.timeout(1) # EXECUTE cycle 260 yield self.env.timeout(1) # EMIT cycle 261 self._do_emit_new(inst, result, False, token.act_id, frame_id) 262 elif inst.opcode == RoutingOp.ALLOC_REMOTE: 263 # PE-level: read target PE, act_id, and optional parent act_id from frame constants 264 # fref+0: target PE 265 # fref+1: target act_id 266 # fref+2: parent act_id (0 = fresh ALLOC, non-zero = ALLOC_SHARED) 267 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) 268 target_pe = self.frames[frame_id][inst.fref] if inst.fref < len(self.frames[frame_id]) else 0 269 target_act = self.frames[frame_id][inst.fref + 1] if inst.fref + 1 < len(self.frames[frame_id]) else 0 270 parent_act = self.frames[frame_id][inst.fref + 2] if inst.fref + 2 < len(self.frames[frame_id]) else 0 271 272 # Guard against None slot values 273 if target_pe is None or target_act is None: 274 logger.warning(f"PE {self.pe_id}: ALLOC_REMOTE has None at fref slots, skipping") 275 return 276 277 if parent_act: 278 alloc_op = FrameOp.ALLOC_SHARED 279 payload = parent_act 280 else: 281 alloc_op = FrameOp.ALLOC 282 payload = 0 283 284 fct = FrameControlToken( 285 target=target_pe, 286 act_id=target_act, 287 op=alloc_op, 288 payload=payload, 289 ) 290 self._on_event(Executed( 291 time=self.env.now, component=self._component, 292 op=inst.opcode, result=0, bool_out=False, 293 )) 294 yield self.env.timeout(1) # EXECUTE cycle 295 yield self.env.timeout(1) # EMIT cycle 296 self.env.process(self._deliver(self.route_table[target_pe], fct)) 297 elif inst.opcode == RoutingOp.FREE_FRAME: 298 # Deallocate frame: compute and free, then EMIT cycle (no output token) 299 # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) 300 result, bool_out = execute(inst.opcode, left, right, None) 301 self._on_event(Executed( 302 time=self.env.now, component=self._component, 303 op=inst.opcode, result=result, bool_out=bool_out, 304 )) 305 yield self.env.timeout(1) # EXECUTE cycle 306 yield self.env.timeout(1) # EMIT cycle (no output token) 307 # Frame deallocation happens during EMIT cycle with smart FREE logic 308 if token.act_id in self.tag_store: 309 self._smart_free(token.act_id) 310 else: 311 logger.warning(f"PE {self.pe_id}: FREE_FRAME for unknown act_id {token.act_id}") 312 else: 313 # Normal ALU execute 314 # MINOR FIX: Restructure const_val handling to avoid dead code 315 const_val = None 316 if inst.has_const and inst.fref < len(self.frames[frame_id]): 317 const_val = self.frames[frame_id][inst.fref] 318 if not isinstance(const_val, int): 319 const_val = None 320 result, bool_out = execute(inst.opcode, left, right, const_val) 321 self._on_event(Executed( 322 time=self.env.now, component=self._component, 323 op=inst.opcode, result=result, bool_out=bool_out, 324 )) 325 yield self.env.timeout(1) # EXECUTE cycle 326 yield self.env.timeout(1) # EMIT cycle 327 self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left) 328 329 def _smart_free(self, act_id: int) -> None: 330 """Smart FREE helper: deallocate lane, possibly returning frame to free list. 331 332 Does NOT yield. Caller handles timing. Emits FrameFreed event. 333 """ 334 if act_id not in self.tag_store: 335 return # Caller should have checked, but skip silently 336 337 frame_id, lane = self.tag_store.pop(act_id) 338 # Clear this lane's match state 339 for i in range(self.matchable_offsets): 340 self.match_data[frame_id][i][lane] = None 341 self.presence[frame_id][i][lane] = False 342 self.port_store[frame_id][i][lane] = None 343 # Check if any other activations use this frame 344 frame_in_use = any(fid == frame_id for fid, _ in self.tag_store.values()) 345 if frame_in_use: 346 # Return lane to pool, keep frame 347 self.lane_free[frame_id].add(lane) 348 self._on_event(FrameFreed( 349 time=self.env.now, component=self._component, 350 act_id=act_id, frame_id=frame_id, 351 lane=lane, frame_freed=False, 352 )) 353 else: 354 # Last lane — return frame to free list 355 self.free_frames.append(frame_id) 356 if frame_id in self.lane_free: 357 del self.lane_free[frame_id] 358 # Clear frame slots 359 for i in range(self.frame_slots): 360 self.frames[frame_id][i] = None 361 self._on_event(FrameFreed( 362 time=self.env.now, component=self._component, 363 act_id=act_id, frame_id=frame_id, 364 lane=lane, frame_freed=True, 365 )) 366 367 def _handle_frame_control(self, token: FrameControlToken) -> None: 368 """Handle ALLOC, FREE, ALLOC_SHARED, and FREE_LANE operations.""" 369 if token.op == FrameOp.ALLOC: 370 if self.free_frames: 371 frame_id = self.free_frames.pop() 372 self.tag_store[token.act_id] = (frame_id, 0) 373 # Set up lane tracking: lane 0 is taken, rest are free 374 self.lane_free[frame_id] = set(range(1, self.lane_count)) 375 # Initialize frame slots to None 376 for i in range(self.frame_slots): 377 self.frames[frame_id][i] = None 378 # Reset all lanes' match state 379 for i in range(self.matchable_offsets): 380 for ln in range(self.lane_count): 381 self.match_data[frame_id][i][ln] = None 382 self.presence[frame_id][i][ln] = False 383 self.port_store[frame_id][i][ln] = None 384 self._on_event(FrameAllocated( 385 time=self.env.now, component=self._component, 386 act_id=token.act_id, frame_id=frame_id, lane=0, 387 )) 388 else: 389 logger.warning(f"PE {self.pe_id}: no free frames available") 390 elif token.op == FrameOp.FREE: 391 if token.act_id in self.tag_store: 392 self._smart_free(token.act_id) 393 else: 394 logger.warning(f"PE {self.pe_id}: FREE for unknown act_id {token.act_id}") 395 elif token.op == FrameOp.ALLOC_SHARED: 396 # Shared allocation: find parent's frame, assign next free lane 397 # Guard against self-referential act_id (would leak old lane) 398 if token.act_id in self.tag_store: 399 self._on_event(TokenRejected( 400 time=self.env.now, component=self._component, 401 token=token, reason=f"act_id {token.act_id} already in tag store", 402 )) 403 return 404 parent_act_id = token.payload 405 if parent_act_id not in self.tag_store: 406 self._on_event(TokenRejected( 407 time=self.env.now, component=self._component, 408 token=token, reason=f"parent act_id {parent_act_id} not in tag store", 409 )) 410 return 411 parent_frame_id, _ = self.tag_store[parent_act_id] 412 free_lanes = self.lane_free.get(parent_frame_id, set()) 413 if not free_lanes: 414 self._on_event(TokenRejected( 415 time=self.env.now, component=self._component, 416 token=token, reason="no free lanes", 417 )) 418 return 419 lane = min(free_lanes) # Deterministic: pick lowest free lane 420 free_lanes.remove(lane) 421 self.tag_store[token.act_id] = (parent_frame_id, lane) 422 # Clear only this lane's match state 423 for i in range(self.matchable_offsets): 424 self.match_data[parent_frame_id][i][lane] = None 425 self.presence[parent_frame_id][i][lane] = False 426 self.port_store[parent_frame_id][i][lane] = None 427 self._on_event(FrameAllocated( 428 time=self.env.now, component=self._component, 429 act_id=token.act_id, frame_id=parent_frame_id, lane=lane, 430 )) 431 elif token.op == FrameOp.FREE_LANE: 432 # Free lane with smart frame deallocation. 433 # If this is the last lane using the frame, the frame is returned to free_frames. 434 # Otherwise, just the lane is returned to the pool. 435 if token.act_id in self.tag_store: 436 self._smart_free(token.act_id) 437 else: 438 logger.warning(f"PE {self.pe_id}: FREE_LANE for unknown act_id {token.act_id}") 439 440 def _handle_local_write(self, token: PELocalWriteToken) -> None: 441 """Handle IRAM write and frame write.""" 442 if token.region == 0: # IRAM 443 self.iram[token.slot] = unpack_instruction(token.data) 444 self._on_event(IRAMWritten( 445 time=self.env.now, component=self._component, 446 offset=token.slot, count=1, 447 )) 448 elif token.region == 1: # Frame 449 if token.act_id in self.tag_store: 450 frame_id, _lane = self.tag_store[token.act_id] 451 if token.is_dest: 452 # Decode flit 1 to FrameDest 453 dest = unpack_flit1(token.data) 454 self.frames[frame_id][token.slot] = dest 455 else: 456 # Store as int 457 self.frames[frame_id][token.slot] = token.data 458 self._on_event(FrameSlotWritten( 459 time=self.env.now, component=self._component, 460 frame_id=frame_id, slot=token.slot, 461 value=token.data if not token.is_dest else None, 462 )) 463 else: 464 # MINOR FIX: Emit TokenRejected for invalid act_id, consistent with other paths 465 logger.warning(f"PE {self.pe_id}: PELocalWriteToken with invalid act_id {token.act_id}") 466 self._on_event(TokenRejected( 467 time=self.env.now, component=self._component, 468 token=token, reason=f"act_id {token.act_id} not in tag store", 469 )) 470 471 def _match_frame( 472 self, 473 token: DyadToken, 474 inst: Instruction, 475 frame_id: int, 476 lane: int, 477 ) -> Optional[tuple[int, int]]: 478 """Frame-based dyadic matching with lane support. 479 480 Derives match slot from low bits of token.offset: 481 match_slot = token.offset % matchable_offsets 482 483 Match data, presence, and port are per-lane. 484 Frame constants/destinations remain shared. 485 """ 486 match_slot = token.offset % self.matchable_offsets 487 488 if self.presence[frame_id][match_slot][lane]: 489 # Partner already waiting — pair them 490 partner_data = self.match_data[frame_id][match_slot][lane] 491 partner_port = self.port_store[frame_id][match_slot][lane] 492 self.presence[frame_id][match_slot][lane] = False 493 self.match_data[frame_id][match_slot][lane] = None 494 495 # Use port metadata to determine left/right ordering 496 if partner_port == Port.L: 497 left, right = partner_data, token.data 498 else: 499 left, right = token.data, partner_data 500 501 self._on_event(Matched( 502 time=self.env.now, component=self._component, 503 left=left, right=right, act_id=token.act_id, 504 offset=token.offset, frame_id=frame_id, 505 )) 506 return left, right 507 else: 508 # Store and wait for partner 509 self.match_data[frame_id][match_slot][lane] = token.data 510 self.port_store[frame_id][match_slot][lane] = token.port 511 self.presence[frame_id][match_slot][lane] = True 512 return None 513 514 def _do_emit_new( 515 self, 516 inst: Instruction, 517 result: int, 518 bool_out: bool, 519 act_id: int, 520 frame_id: int, 521 left: int = 0, 522 ) -> None: 523 """Mode-driven output routing. 524 525 Reads OutputStyle from instruction and delegates to appropriate handler. 526 Suppresses output for GATE when bool_out=False. 527 """ 528 if isinstance(inst.opcode, RoutingOp) and inst.opcode == RoutingOp.GATE and not bool_out: 529 return # GATE suppressed 530 531 match inst.output: 532 case OutputStyle.INHERIT: 533 self._emit_inherit(inst, result, bool_out, frame_id) 534 case OutputStyle.CHANGE_TAG: 535 self._emit_change_tag(inst, result, left) 536 case OutputStyle.SINK: 537 self._emit_sink(inst, result, frame_id) 538 539 def _emit_inherit( 540 self, 541 inst: Instruction, 542 result: int, 543 bool_out: bool, 544 frame_id: int, 545 ) -> None: 546 """INHERIT output: read FrameDest from frame and route token. 547 548 Frame layout per mode table: 549 - Mode 0: [dest] 550 - Mode 1: [const, dest] 551 - Mode 2: [dest1, dest2] 552 - Mode 3: [const, dest1, dest2] 553 554 CRITICAL FIX: Check for switch ops BEFORE emitting dest_l to avoid spawning 555 delivery processes that cannot be cancelled. 556 """ 557 dest_base = inst.fref + (1 if inst.has_const else 0) 558 559 # CRITICAL FIX: Check for switch ops first 560 is_switch = (isinstance(inst.opcode, RoutingOp) and inst.opcode in ( 561 RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, 562 )) 563 564 # Handle switch ops specially: emit both outputs at once based on bool_out 565 if is_switch and inst.dest_count >= 2: 566 dest_l = self.frames[frame_id][dest_base] 567 dest_r = self.frames[frame_id][dest_base + 1] 568 if isinstance(dest_l, FrameDest) and isinstance(dest_r, FrameDest): 569 if bool_out: 570 taken, not_taken = dest_l, dest_r 571 else: 572 taken, not_taken = dest_r, dest_l 573 data_tok = self._make_token_from_dest(taken, result) 574 trig_tok = self._make_token_from_dest(not_taken, 0) 575 self.output_log.append(data_tok) 576 self.output_log.append(trig_tok) 577 self._on_event(Emitted( 578 time=self.env.now, component=self._component, token=data_tok, 579 )) 580 self._on_event(Emitted( 581 time=self.env.now, component=self._component, token=trig_tok, 582 )) 583 self.env.process(self._deliver(self.route_table[taken.target_pe], data_tok)) 584 self.env.process(self._deliver(self.route_table[not_taken.target_pe], trig_tok)) 585 return 586 587 # Non-switch path: emit normally 588 if inst.dest_count >= 1: 589 dest_l = self.frames[frame_id][dest_base] 590 if isinstance(dest_l, FrameDest): 591 out_token = self._make_token_from_dest(dest_l, result) 592 self.output_log.append(out_token) 593 self._on_event(Emitted( 594 time=self.env.now, component=self._component, token=out_token, 595 )) 596 self.env.process(self._deliver(self.route_table[dest_l.target_pe], out_token)) 597 else: 598 logger.warning("PE %d: frame[%d][%d] is not FrameDest: %r", 599 self.pe_id, frame_id, dest_base, dest_l) 600 601 if inst.dest_count >= 2: 602 dest_r = self.frames[frame_id][dest_base + 1] 603 if isinstance(dest_r, FrameDest): 604 out_r = self._make_token_from_dest(dest_r, result) 605 self.output_log.append(out_r) 606 self._on_event(Emitted( 607 time=self.env.now, component=self._component, token=out_r, 608 )) 609 self.env.process(self._deliver( 610 self.route_table[dest_r.target_pe], out_r, 611 )) 612 else: 613 logger.warning("PE %d: frame[%d][%d] is not FrameDest: %r", 614 self.pe_id, frame_id, dest_base + 1, dest_r) 615 616 def _emit_change_tag( 617 self, 618 inst: Instruction, 619 result: int, 620 left: int, 621 ) -> None: 622 """CHANGE_TAG output: unpack left operand (flit 1) to get destination.""" 623 dest = unpack_flit1(left) 624 out_token = self._make_token_from_dest(dest, result) 625 self.output_log.append(out_token) 626 self._on_event(Emitted( 627 time=self.env.now, component=self._component, token=out_token, 628 )) 629 self.env.process(self._deliver(self.route_table[dest.target_pe], out_token)) 630 631 def _emit_sink(self, inst: Instruction, result: int, frame_id: int) -> None: 632 """SINK output: write result to frame slot, emit no token.""" 633 self.frames[frame_id][inst.fref] = result 634 self._on_event(FrameSlotWritten( 635 time=self.env.now, component=self._component, 636 frame_id=frame_id, slot=inst.fref, value=result, 637 )) 638 639 def _build_and_emit_sm_new( 640 self, 641 inst: Instruction, 642 left: int, 643 right: Optional[int], 644 act_id: int, 645 frame_id: int, 646 ) -> None: 647 """Build and emit SM token. 648 649 Return route is a FrameDest stored at inst.fref + (1 if has_const else 0). 650 SM target comes from frame[fref] (if has_const) or from left operand. 651 """ 652 ret_slot = inst.fref + (1 if inst.has_const else 0) 653 ret_dest = self.frames[frame_id][ret_slot] if inst.dest_count > 0 else None 654 655 # Build return CMToken from FrameDest if return route exists 656 ret_token = None 657 if isinstance(ret_dest, FrameDest): 658 ret_token = self._make_token_from_dest(ret_dest, 0) 659 660 # Determine SM target source 661 if inst.has_const: 662 target_packed = self.frames[frame_id][inst.fref] 663 else: 664 target_packed = left 665 666 sm_token = SMToken( 667 target=(target_packed >> 8) & 0xFF, 668 addr=target_packed & 0xFF, 669 op=inst.opcode, 670 flags=right if right is not None else None, 671 data=right if inst.has_const else left, 672 ret=ret_token, 673 ) 674 self.output_log.append(sm_token) 675 self._on_event(Emitted( 676 time=self.env.now, component=self._component, token=sm_token, 677 )) 678 self.env.process(self._deliver(self.sm_routes[sm_token.target], sm_token)) 679 680 def _make_token_from_dest(self, dest: FrameDest, data: int) -> CMToken: 681 """Construct CMToken from FrameDest and data.""" 682 match dest.token_kind: 683 case TokenKind.DYADIC: 684 return DyadToken( 685 target=dest.target_pe, offset=dest.offset, 686 act_id=dest.act_id, data=data, 687 port=dest.port, 688 ) 689 case TokenKind.MONADIC: 690 return MonadToken( 691 target=dest.target_pe, offset=dest.offset, 692 act_id=dest.act_id, data=data, inline=False, 693 ) 694 case TokenKind.INLINE: 695 return MonadToken( 696 target=dest.target_pe, offset=dest.offset, 697 act_id=dest.act_id, data=data, inline=True, 698 ) 699 case _: 700 raise ValueError(f"unknown token_kind: {dest.token_kind}") 701 702 def _deliver(self, store: simpy.Store, token: Token) -> None: 703 """Spawn delivery process: 1 cycle delay, then put token. 704 705 Accepts any Token type (CMToken, SMToken, FrameControlToken) since all are delivered to stores. 706 """ 707 yield self.env.timeout(1) 708 yield store.put(token)