diff --git a/emu/pe.py b/emu/pe.py index a80a713..881cab9 100644 --- a/emu/pe.py +++ b/emu/pe.py @@ -180,13 +180,18 @@ class ProcessingElement: # EXECUTE & EMIT depends on opcode type if isinstance(inst.opcode, MemOp): - # SM dispatch - yield self.env.timeout(1) + # SM dispatch: EXECUTE cycle computes, then EMIT cycle delivers + # Total: 4 cycles for monadic (dequeue + IFETCH + EXECUTE + EMIT) + self._on_event(Executed( + time=self.env.now, component=self._component, + op=inst.opcode, result=0, bool_out=False, + )) + yield self.env.timeout(1) # EXECUTE cycle + yield self.env.timeout(1) # EMIT cycle self._build_and_emit_sm_new(inst, left, right, token.act_id, frame_id) - yield self.env.timeout(1) elif inst.opcode == RoutingOp.EXTRACT_TAG: # PE-level: pack current PE/act_id/offset into flit 1 - yield self.env.timeout(1) + # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) result = pack_flit1(FrameDest( target_pe=self.pe_id, offset=token.offset, @@ -198,11 +203,12 @@ class ProcessingElement: time=self.env.now, component=self._component, op=inst.opcode, result=result, bool_out=False, )) + yield self.env.timeout(1) # EXECUTE cycle + yield self.env.timeout(1) # EMIT cycle self._do_emit_new(inst, result, False, token.act_id, frame_id) - yield self.env.timeout(1) elif inst.opcode == RoutingOp.ALLOC_REMOTE: # PE-level: read target PE and act_id from frame constants - yield self.env.timeout(1) + # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) target_pe = self.frames[frame_id][inst.fref] target_act = self.frames[frame_id][inst.fref + 1] fct = FrameControlToken( @@ -215,16 +221,20 @@ class ProcessingElement: time=self.env.now, component=self._component, op=inst.opcode, result=0, bool_out=False, )) + yield self.env.timeout(1) # EXECUTE cycle + yield self.env.timeout(1) # EMIT cycle self.env.process(self._deliver(self.route_table[target_pe], fct)) - yield self.env.timeout(1) elif inst.opcode == RoutingOp.FREE_FRAME: - # Deallocate frame - yield self.env.timeout(1) + # Deallocate frame: compute and free, then EMIT cycle (no output token) + # Total: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) result, bool_out = execute(inst.opcode, left, right, None) self._on_event(Executed( time=self.env.now, component=self._component, op=inst.opcode, result=result, bool_out=bool_out, )) + yield self.env.timeout(1) # EXECUTE cycle + yield self.env.timeout(1) # EMIT cycle (no output token) + # Frame deallocation happens during EMIT cycle if token.act_id in self.tag_store: freed_frame = self.tag_store.pop(token.act_id) self.free_frames.append(freed_frame) @@ -232,8 +242,6 @@ class ProcessingElement: time=self.env.now, component=self._component, act_id=token.act_id, frame_id=freed_frame, )) - # No emit — FREE_FRAME suppresses output - yield self.env.timeout(1) else: # Normal ALU execute # MINOR FIX: Restructure const_val handling to avoid dead code @@ -245,9 +253,8 @@ class ProcessingElement: time=self.env.now, component=self._component, op=inst.opcode, result=result, bool_out=bool_out, )) - yield self.env.timeout(1) - # EMIT cycle: complete the cycle before firing Emitted events - yield self.env.timeout(1) + yield self.env.timeout(1) # EXECUTE cycle + yield self.env.timeout(1) # EMIT cycle self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left) def _handle_frame_control(self, token: FrameControlToken) -> None: diff --git a/tests/test_pe_frames.py b/tests/test_pe_frames.py index d9799f6..1cce450 100644 --- a/tests/test_pe_frames.py +++ b/tests/test_pe_frames.py @@ -1263,3 +1263,284 @@ class TestPipelineTiming: assert frame_allocated_time is not None, "Should have FrameAllocated event" delta = frame_allocated_time - injection_time assert delta == 2, f"Expected 2 cycles for side path, got {delta}" + + def test_extract_tag_timing(self): + """Verify EXTRACT_TAG timing: 4 cycles from injection to Emitted event. + + EXTRACT_TAG is a monadic special path that packs PE/offset/act_id into flit 1. + Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. + """ + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=2, + config=config, + ) + + # Allocate frame + fct = FrameControlToken(target=0, act_id=5, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[5] + + # Set up EXTRACT_TAG instruction + inst = Instruction( + opcode=RoutingOp.EXTRACT_TAG, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=1, + wide=False, + fref=0, + ) + pe.iram[200] = inst + + # Set destination for the packed flit 1 result + dest = FrameDest( + target_pe=1, + offset=201, + act_id=5, + port=Port.L, + token_kind=TokenKind.DYADIC, + ) + pe.frames[frame_id][0] = dest + + # Wire route table + pe.route_table[1] = simpy.Store(env) + + # Record time before injecting + injection_time = env.now + + # Inject monadic token to EXTRACT_TAG + tok = MonadToken( + target=0, + offset=200, + act_id=5, + data=99, + inline=False, + ) + def _put(): + yield pe.input_store.put(tok) + env.process(_put()) + env.run() + + # Find the Emitted event + emitted_time = None + for e in events: + if isinstance(e, Emitted): + emitted_time = e.time + break + + # Verify timing: 4 cycles from injection to emission + assert emitted_time is not None, "Should have Emitted event for EXTRACT_TAG result" + delta = emitted_time - injection_time + assert delta == 4, f"Expected 4 cycles for EXTRACT_TAG, got {delta}" + + def test_sm_dispatch_timing(self): + """Verify SM dispatch timing: 4 cycles from injection to Emitted event. + + SM operations are monadic and emit SMToken at EMIT stage. + Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. + """ + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Allocate frame + fct = FrameControlToken(target=0, act_id=7, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[7] + + # Set up SM READ instruction (monadic in terms of PE pipeline) + inst = Instruction( + opcode=MemOp.READ, + output=OutputStyle.INHERIT, + has_const=True, + dest_count=1, + wide=False, + fref=0, + ) + pe.iram[300] = inst + + # Set constant (SM target+address) and destination (return route) + pe.frames[frame_id][0] = (3 << 8) | 42 # SM 3, address 42 + ret_dest = FrameDest( + target_pe=0, + offset=301, + act_id=7, + port=Port.L, + token_kind=TokenKind.MONADIC, + ) + pe.frames[frame_id][1] = ret_dest + + # Wire route table and SM routes + pe.route_table[0] = simpy.Store(env) + pe.sm_routes[3] = simpy.Store(env) + + # Record time before injecting + injection_time = env.now + + # Inject monadic token to SM instruction + tok = MonadToken( + target=0, + offset=300, + act_id=7, + data=55, + inline=False, + ) + def _put(): + yield pe.input_store.put(tok) + env.process(_put()) + env.run() + + # Find the Emitted event (SMToken emission) + emitted_time = None + for e in events: + if isinstance(e, Emitted): + emitted_time = e.time + break + + # Verify timing: 4 cycles from injection to SMToken emission + assert emitted_time is not None, "Should have Emitted event for SMToken" + delta = emitted_time - injection_time + assert delta == 4, f"Expected 4 cycles for SM dispatch, got {delta}" + + def test_free_frame_timing(self): + """Verify FREE_FRAME timing: 4 cycles from injection to FrameFreed event. + + FREE_FRAME deallocates a frame and suppresses output token. + Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. + """ + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Allocate frame + fct = FrameControlToken(target=0, act_id=10, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[10] + + # Set up FREE_FRAME instruction + inst = Instruction( + opcode=RoutingOp.FREE_FRAME, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=0, + wide=False, + fref=0, + ) + pe.iram[400] = inst + + # Record time before injecting + injection_time = env.now + + # Inject monadic token to FREE_FRAME + tok = MonadToken( + target=0, + offset=400, + act_id=10, + data=0, + inline=False, + ) + def _put(): + yield pe.input_store.put(tok) + env.process(_put()) + env.run() + + # Find the FrameFreed event + frame_freed_time = None + for e in events: + if isinstance(e, FrameFreed): + frame_freed_time = e.time + break + + # Verify timing: 4 cycles from injection to FrameFreed event + assert frame_freed_time is not None, "Should have FrameFreed event" + delta = frame_freed_time - injection_time + assert delta == 4, f"Expected 4 cycles for FREE_FRAME, got {delta}" + # Also verify frame was actually freed + assert 10 not in pe.tag_store, "Frame should be freed from tag_store" + assert frame_id in pe.free_frames, "Frame should be returned to free_frames" + + def test_alloc_remote_timing(self): + """Verify ALLOC_REMOTE timing: 4 cycles from injection to delivery. + + ALLOC_REMOTE constructs a FrameControlToken and routes it to target PE. + Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. + The delivery process (_deliver) adds 1 more cycle after EMIT completes. + """ + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Allocate frame + fct = FrameControlToken(target=0, act_id=12, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[12] + + # Set up ALLOC_REMOTE instruction + inst = Instruction( + opcode=RoutingOp.ALLOC_REMOTE, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=0, + wide=False, + fref=0, + ) + pe.iram[500] = inst + + # Set target PE and target act_id in frame slots + pe.frames[frame_id][0] = 1 # target PE 1 + pe.frames[frame_id][1] = 20 # target act_id 20 + + # Wire route table for target PE + target_store = simpy.Store(env) + pe.route_table[1] = target_store + + # Record time before injecting + injection_time = env.now + + # Inject monadic token to ALLOC_REMOTE + tok = MonadToken( + target=0, + offset=500, + act_id=12, + data=0, + inline=False, + ) + def _put(): + yield pe.input_store.put(tok) + env.process(_put()) + env.run() + + # The Executed event fires right before the EXECUTE yield, at time: dequeue(1) + IFETCH(1) = 2 + # The EMIT cycle completes at inject + 4 + # The _deliver process then yields 1 more cycle before putting to target_store + # So FrameControlToken arrives at target_store at inject + 5 cycles total + + # For this test, we verify the Executed event timing + executed_time = None + for e in events: + if isinstance(e, Executed) and e.op == RoutingOp.ALLOC_REMOTE: + executed_time = e.time + break + + assert executed_time is not None, "Should have Executed event for ALLOC_REMOTE" + # Executed fires after IFETCH (dequeue 1 + IFETCH 1 = 2) + delta = executed_time - injection_time + assert delta == 2, f"Expected Executed at 2 cycles, got {delta}"