diff --git a/emu/alu.py b/emu/alu.py --- a/emu/alu.py +++ b/emu/alu.py @@ -67,11 +67,11 @@ result = (left + 1) & UINT16_MASK case ArithOp.DEC: result = (left - 1) & UINT16_MASK - case ArithOp.SHIFT_L: + case ArithOp.SHL: result = (left << const) & UINT16_MASK - case ArithOp.SHIFT_R: + case ArithOp.SHR: result = (left >> const) & UINT16_MASK - case ArithOp.ASHFT_R: + case ArithOp.ASR: signed = to_signed(left) result = (signed >> const) & UINT16_MASK case _: @@ -132,7 +132,11 @@ return left, False case RoutingOp.CONST: return const & UINT16_MASK, False - case RoutingOp.FREE_CTX: + case RoutingOp.FREE_FRAME: + return 0, False + case RoutingOp.EXTRACT_TAG: + return 0, False + case RoutingOp.ALLOC_REMOTE: return 0, False case RoutingOp.SEL: cmp = left != 0 diff --git a/emu/pe.py b/emu/pe.py --- a/emu/pe.py +++ b/emu/pe.py @@ -1,299 +1,524 @@ +""" +Frame-based ProcessingElement for OR1 dataflow CPU. + +Implements: +- Frame-based matching with tag_store + presence bits +- Mode-driven output routing (INHERIT, CHANGE_TAG, SINK) +- PE-level EXTRACT_TAG and ALLOC_REMOTE handling +- Side path handling for PELocalWriteToken and FrameControlToken +- Cycle-accurate pipeline: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths +""" + import logging -from enum import Enum from typing import Optional import simpy -from cm_inst import ALUInst, Addr, ArithOp, LogicOp, MemOp, Port, RoutingOp, SMInst, is_monadic_alu +from cm_inst import ( + ALUOp, ArithOp, FrameDest, FrameOp, FrameSlotValue, + Instruction, LogicOp, MemOp, OutputStyle, Port, RoutingOp, + TokenKind, is_monadic_alu, +) +from encoding import pack_flit1, unpack_flit1, unpack_instruction from emu.alu import execute from emu.events import ( - EventCallback, TokenReceived, Matched, Executed, Emitted, IRAMWritten, + Emitted, EventCallback, Executed, FrameAllocated, FrameFreed, + FrameSlotWritten, IRAMWritten, Matched, TokenReceived, TokenRejected, ) -from emu.types import MatchEntry +from emu.types import PEConfig from tokens import ( - CMToken, DyadToken, IRAMWriteToken, - MonadToken, SMToken, + CMToken, DyadToken, FrameControlToken, + MonadToken, PELocalWriteToken, PEToken, SMToken, ) logger = logging.getLogger(__name__) -class OutputMode(Enum): - """Output routing mode for ALU instructions.""" - SUPPRESS = "SUPPRESS" - SINGLE = "SINGLE" - DUAL = "DUAL" - SWITCH = "SWITCH" - - class ProcessingElement: + """Frame-based Processing Element for OR1 dataflow CPU. + + Manages: + - Frame store: [frame_count][frame_slots] dense per-activation data + - Tag store: act_id → frame_id mapping + - Presence bits: [frame_count][matchable_offsets] for dyadic matching + - Port store: [frame_count][matchable_offsets] for port metadata + - Free frames: pool of available frame IDs + + Pipeline (per token): + - Side paths (FrameControlToken, PELocalWriteToken): 1 cycle + - Dyadic CMToken: 5 cycles (dequeue + IFETCH + MATCH + EXECUTE + EMIT) + - Monadic CMToken: 4 cycles (dequeue + IFETCH + EXECUTE + EMIT) + """ + def __init__( self, env: simpy.Environment, pe_id: int, - iram: dict[int, ALUInst | SMInst], - ctx_slots: int = 4, - offsets: int = 64, - fifo_capacity: int = 8, - on_event: EventCallback | None = None, + config: PEConfig, ): self.env = env self.pe_id = pe_id - self.iram = iram - self.input_store: simpy.Store = simpy.Store(env, capacity=fifo_capacity) + self.frame_count = config.frame_count + self.frame_slots = config.frame_slots + self.matchable_offsets = config.matchable_offsets + + # Frame storage + self.frames: list[list[Optional[FrameSlotValue]]] = [ + [None for _ in range(config.frame_slots)] + for _ in range(config.frame_count) + ] + + # Tag store: act_id → frame_id + self.tag_store: dict[int, int] = dict(config.initial_tag_store or {}) + + # Presence bits: [frame_id][match_slot] - True if operand waiting for partner + self.presence: list[list[bool]] = [ + [False for _ in range(config.matchable_offsets)] + for _ in range(config.frame_count) + ] + + # Port store: [frame_id][match_slot] - port of waiting operand + self.port_store: list[list[Optional[Port]]] = [ + [None for _ in range(config.matchable_offsets)] + for _ in range(config.frame_count) + ] + + # Free frames pool + self.free_frames = list(range(config.frame_count)) + for frame_id in self.tag_store.values(): + if frame_id in self.free_frames: + self.free_frames.remove(frame_id) + + # IRAM + self.iram: dict[int, Instruction] = config.iram or {} + + # Network routing + self.input_store: simpy.Store = simpy.Store(env) self.route_table: dict[int, simpy.Store] = {} self.sm_routes: dict[int, simpy.Store] = {} - self.matching_store: list[list[MatchEntry]] = [ - [MatchEntry() for _ in range(offsets)] - for _ in range(ctx_slots) - ] - self.gen_counters: list[int] = [0] * ctx_slots - self._ctx_slots = ctx_slots - self._offsets = offsets - self._on_event: EventCallback = on_event or (lambda _: None) + + # Observability + self._on_event: EventCallback = config.on_event or (lambda _: None) self._component = f"pe:{pe_id}" self.output_log: list = [] + + # Start main process self.process = env.process(self._run()) - def _run(self): + def _run(self) -> None: + """Main loop: dequeue token, emit TokenReceived, spawn processor.""" while True: token = yield self.input_store.get() yield self.env.timeout(1) # dequeue cycle - self._on_event(TokenReceived(time=self.env.now, component=self._component, token=token)) + self._on_event(TokenReceived( + time=self.env.now, component=self._component, token=token, + )) self.env.process(self._process_token(token)) - def _process_token(self, token): - if isinstance(token, IRAMWriteToken): - self._handle_iram_write(token) - yield self.env.timeout(1) # write cycle + def _process_token(self, token: PEToken) -> None: + """Process a single token through the pipeline. + + Dispatches to side paths (FrameControlToken, PELocalWriteToken) or + CMToken pipeline (IFETCH → act_id resolution → MATCH → EXECUTE → EMIT). + """ + if isinstance(token, FrameControlToken): + self._handle_frame_control(token) + yield self.env.timeout(1) return - if isinstance(token, MonadToken): - operands = self._match_monadic(token) - elif isinstance(token, DyadToken): - inst = self._fetch(token.offset) - if inst is not None and self._is_monadic_instruction(inst): - operands = (token.data, None) - else: - # match cycle - operands = self._match_dyadic(token) - yield self.env.timeout(1) - else: - logger.warning("PE%d: unknown token type: %s", self.pe_id, type(token)) + if isinstance(token, PELocalWriteToken): + self._handle_local_write(token) + yield self.env.timeout(1) return - if operands is None: + # CMToken pipeline: IFETCH → act_id resolution → MATCH → EXECUTE → EMIT + if not isinstance(token, CMToken): + logger.warning(f"PE {self.pe_id}: unknown token type {type(token)}") return - left, right = operands - - # fetch cycle - inst = self._fetch(token.offset) + # IFETCH (1 cycle) + inst = self.iram.get(token.offset) yield self.env.timeout(1) if inst is None: - logger.warning("PE%d: no IRAM entry at offset %d", self.pe_id, token.offset) + logger.warning(f"PE {self.pe_id}: no instruction at offset {token.offset}") return - if isinstance(inst, SMInst): - # execute cycle (build SM token) - yield self.env.timeout(1) - # emit cycle (spawn delivery process) - self._build_and_emit_sm(inst, left, right, token.ctx) - yield self.env.timeout(1) + # Act_id resolution (no cycle - just validation) + if token.act_id not in self.tag_store: + self._on_event(TokenRejected( + time=self.env.now, component=self._component, + token=token, reason=f"act_id {token.act_id} not in tag store", + )) + return + + frame_id = self.tag_store[token.act_id] + + # Determine if monadic or dyadic instruction + is_monadic = ( + isinstance(token, MonadToken) or + (isinstance(token, DyadToken) and ( + isinstance(inst.opcode, MemOp) or + (isinstance(inst.opcode, ALUOp) and is_monadic_alu(inst.opcode)) + )) + ) + + # MATCH (1 cycle for dyadic, 0 for monadic) + if isinstance(token, MonadToken): + left, right = token.data, None + elif isinstance(token, DyadToken): + if is_monadic: + left, right = token.data, None + else: + # Dyadic matching via presence bits + operands = self._match_frame(token, inst, frame_id) + yield self.env.timeout(1) # match cycle + if operands is None: + return # waiting for partner + left, right = operands else: - # execute cycle - result, bool_out = execute(inst.op, left, right, inst.const) + return + + # EXECUTE & EMIT depends on opcode type + if isinstance(inst.opcode, MemOp): + # SM dispatch + yield self.env.timeout(1) + 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) + result = pack_flit1(FrameDest( + target_pe=self.pe_id, + offset=token.offset, + act_id=token.act_id, + port=Port.L, + token_kind=TokenKind.DYADIC, + )) self._on_event(Executed( time=self.env.now, component=self._component, - op=inst.op, result=result, bool_out=bool_out, + op=inst.opcode, result=result, bool_out=False, + )) + 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) + target_pe = self.frames[frame_id][inst.fref] + target_act = self.frames[frame_id][inst.fref + 1] + fct = FrameControlToken( + target=target_pe, + act_id=target_act, + op=FrameOp.ALLOC, + payload=0, + ) + self._on_event(Executed( + time=self.env.now, component=self._component, + op=inst.opcode, result=0, bool_out=False, + )) + 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) + 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, + )) + if token.act_id in self.tag_store: + freed_frame = self.tag_store.pop(token.act_id) + self.free_frames.append(freed_frame) + self._on_event(FrameFreed( + 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 + const_val = self.frames[frame_id][inst.fref] if inst.has_const else None + if isinstance(const_val, int): + const_val = const_val + else: + const_val = None + result, bool_out = execute(inst.opcode, left, right, const_val) + 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) - - # emit cycle (spawn delivery process AFTER timeout so delivery - # starts at emit time and arrives 1 cycle later) - self._do_emit(inst, result, bool_out, token.ctx) + self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left) yield self.env.timeout(1) - def _handle_iram_write(self, token: IRAMWriteToken) -> None: - """Write instructions into IRAM at the offset specified by the token.""" - for i, inst in enumerate(token.instructions): - self.iram[token.offset + i] = inst - self._on_event(IRAMWritten( - time=self.env.now, component=self._component, - offset=token.offset, count=len(token.instructions), - )) + def _handle_frame_control(self, token: FrameControlToken) -> None: + """Handle ALLOC and FREE operations.""" + if token.op == FrameOp.ALLOC: + if self.free_frames: + frame_id = self.free_frames.pop() + self.tag_store[token.act_id] = frame_id + # Initialize frame slots to None + for i in range(self.frame_slots): + self.frames[frame_id][i] = None + self._on_event(FrameAllocated( + time=self.env.now, component=self._component, + act_id=token.act_id, frame_id=frame_id, + )) + else: + logger.warning(f"PE {self.pe_id}: no free frames available") + elif token.op == FrameOp.FREE: + if token.act_id in self.tag_store: + frame_id = self.tag_store.pop(token.act_id) + self.free_frames.append(frame_id) + self._on_event(FrameFreed( + time=self.env.now, component=self._component, + act_id=token.act_id, frame_id=frame_id, + )) - def _match_monadic(self, token: MonadToken) -> tuple[int, None]: - return (token.data, None) + def _handle_local_write(self, token: PELocalWriteToken) -> None: + """Handle IRAM write and frame write.""" + if token.region == 0: # IRAM + self.iram[token.slot] = unpack_instruction(token.data) + self._on_event(IRAMWritten( + time=self.env.now, component=self._component, + offset=token.slot, count=1, + )) + elif token.region == 1: # Frame + if token.act_id in self.tag_store: + frame_id = self.tag_store[token.act_id] + if token.is_dest: + # Decode flit 1 to FrameDest + dest = unpack_flit1(token.data) + self.frames[frame_id][token.slot] = dest + else: + # Store as int + self.frames[frame_id][token.slot] = token.data + self._on_event(FrameSlotWritten( + time=self.env.now, component=self._component, + frame_id=frame_id, slot=token.slot, + value=token.data if not token.is_dest else None, + )) - def _match_dyadic(self, token: DyadToken) -> Optional[tuple[int, int]]: - ctx = token.ctx % self._ctx_slots - offset = token.offset % self._offsets + def _match_frame( + self, + token: DyadToken, + inst: Instruction, + frame_id: int, + ) -> Optional[tuple[int, int]]: + """Frame-based dyadic matching. - if token.gen != self.gen_counters[ctx]: - logger.debug( - "PE%d: stale token discarded (gen %d != %d) at ctx=%d off=%d", - self.pe_id, token.gen, self.gen_counters[ctx], ctx, offset, - ) - return None + Derives match slot from low bits of token.offset: + match_slot = token.offset % matchable_offsets - entry = self.matching_store[ctx][offset] - - if not entry.occupied: - entry.occupied = True - entry.data = token.data - entry.port = token.port - return None - - partner_data = entry.data - partner_port = entry.port - entry.occupied = False - entry.data = None - - if partner_port == Port.L: - left, right = partner_data, token.data - else: - left, right = token.data, partner_data - - self._on_event(Matched( - time=self.env.now, component=self._component, - left=left, right=right, ctx=token.ctx % self._ctx_slots, offset=token.offset % self._offsets, - )) - return (left, right) - - def _fetch(self, offset: int) -> Optional[ALUInst | SMInst]: - return self.iram.get(offset) - - def _is_monadic_instruction(self, inst: ALUInst | SMInst) -> bool: - """Check if an instruction expects a single operand. - - When a DyadToken arrives at a monadic instruction's offset, the PE - bypasses the matching store and fires immediately with (data, None). + Both L and R tokens write to frames[frame_id][match_slot]. + Port metadata determines left/right ordering when second arrives. """ - if isinstance(inst, SMInst): - if inst.op == MemOp.WRITE and inst.const is None: - return False - if inst.op == MemOp.CMP_SW: - return False - return True + match_slot = token.offset % self.matchable_offsets - # For ALU instructions, use canonical is_monadic_alu - return is_monadic_alu(inst.op) + if self.presence[frame_id][match_slot]: + # Partner already waiting — pair them + partner_data = self.frames[frame_id][match_slot] + partner_port = self.port_store[frame_id][match_slot] + self.presence[frame_id][match_slot] = False + self.frames[frame_id][match_slot] = None - def _do_emit(self, inst: ALUInst, result: int, bool_out: bool, ctx: int): - mode = self._output_mode(inst, bool_out) - - if mode == OutputMode.SUPPRESS: - return - - # CTX_OVRD: unpack target context and generation from const field - if inst.ctx_mode == 1 and inst.const is not None: - ctx = (inst.const >> 4) & 0xF - # Generation is packed at bits [3:2] but we use the gen_counters - # array for the target context slot (not the packed gen, which is - # the initial gen at codegen time — runtime gen may have advanced) - - if mode == OutputMode.SINGLE: - out_token = self._make_output_token(inst.dest_l, result, ctx) - self.output_log.append(out_token) - self._on_event(Emitted(time=self.env.now, component=self._component, token=out_token)) - self.env.process(self._deliver(self.route_table[inst.dest_l.pe], out_token)) - - elif mode == OutputMode.DUAL: - out_l = self._make_output_token(inst.dest_l, result, ctx) - out_r = self._make_output_token(inst.dest_r, result, ctx) - self.output_log.append(out_l) - self.output_log.append(out_r) - self._on_event(Emitted(time=self.env.now, component=self._component, token=out_l)) - self._on_event(Emitted(time=self.env.now, component=self._component, token=out_r)) - self.env.process(self._deliver(self.route_table[inst.dest_l.pe], out_l)) - self.env.process(self._deliver(self.route_table[inst.dest_r.pe], out_r)) - - elif mode == OutputMode.SWITCH: - if bool_out: - taken, not_taken = inst.dest_l, inst.dest_r + # Use port metadata to determine left/right ordering + if partner_port == Port.L: + left, right = partner_data, token.data else: - taken, not_taken = inst.dest_r, inst.dest_l + left, right = token.data, partner_data - data_token = self._make_output_token(taken, result, ctx) - self.output_log.append(data_token) - self._on_event(Emitted(time=self.env.now, component=self._component, token=data_token)) - self.env.process(self._deliver(self.route_table[taken.pe], data_token)) + self._on_event(Matched( + time=self.env.now, component=self._component, + left=left, right=right, act_id=token.act_id, + offset=token.offset, frame_id=frame_id, + )) + return left, right + else: + # Store and wait for partner + self.frames[frame_id][match_slot] = token.data + self.port_store[frame_id][match_slot] = token.port + self.presence[frame_id][match_slot] = True + return None - trigger_token = MonadToken( - target=not_taken.pe, - offset=not_taken.a, - ctx=ctx, - data=0, - inline=True, - ) - self.output_log.append(trigger_token) - self._on_event(Emitted(time=self.env.now, component=self._component, token=trigger_token)) - self.env.process(self._deliver(self.route_table[not_taken.pe], trigger_token)) + def _do_emit_new( + self, + inst: Instruction, + result: int, + bool_out: bool, + act_id: int, + frame_id: int, + left: int = 0, + ) -> None: + """Mode-driven output routing. - def _build_and_emit_sm(self, inst: SMInst, left: int, right: int | None, ctx: int): - cell_addr = inst.const if inst.const is not None else left - data = left if inst.const is not None else right + Reads OutputStyle from instruction and delegates to appropriate handler. + Suppresses output for GATE when bool_out=False. + """ + if isinstance(inst.opcode, RoutingOp) and inst.opcode == RoutingOp.GATE and not bool_out: + return # GATE suppressed - ret: CMToken | None = None - if inst.ret is not None: - if inst.ret_dyadic: - ret = DyadToken( - target=inst.ret.pe, - offset=inst.ret.a, - ctx=ctx, - data=0, - port=inst.ret.port, - gen=self.gen_counters[ctx], - wide=False, - ) - else: - ret = MonadToken( - target=inst.ret.pe, - offset=inst.ret.a, - ctx=ctx, - data=0, - inline=False, - ) + match inst.output: + case OutputStyle.INHERIT: + self._emit_inherit(inst, result, bool_out, frame_id) + case OutputStyle.CHANGE_TAG: + self._emit_change_tag(inst, result, left) + case OutputStyle.SINK: + self._emit_sink(inst, result, frame_id) + + def _emit_inherit( + self, + inst: Instruction, + result: int, + bool_out: bool, + frame_id: int, + ) -> None: + """INHERIT output: read FrameDest from frame and route token. + + Frame layout per mode table: + - Mode 0: [dest] + - Mode 1: [const, dest] + - Mode 2: [dest1, dest2] + - Mode 3: [const, dest1, dest2] + """ + dest_base = inst.fref + (1 if inst.has_const else 0) + + if inst.dest_count >= 1: + dest_l = self.frames[frame_id][dest_base] + if isinstance(dest_l, FrameDest): + out_token = self._make_token_from_dest(dest_l, result) + self.output_log.append(out_token) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=out_token, + )) + self.env.process(self._deliver(self.route_table[dest_l.target_pe], out_token)) + + if inst.dest_count >= 2: + dest_r = self.frames[frame_id][dest_base + 1] + if isinstance(dest_r, FrameDest): + # For switch ops, route based on bool_out + if isinstance(inst.opcode, RoutingOp) and inst.opcode in ( + RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, + ): + # Undo the dest_l append above — switch re-routes both outputs + if inst.dest_count >= 1 and self.output_log: + self.output_log.pop() + if bool_out: + taken, not_taken = dest_l, dest_r + else: + taken, not_taken = dest_r, dest_l + if isinstance(taken, FrameDest): + data_tok = self._make_token_from_dest(taken, result) + self.output_log.append(data_tok) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=data_tok, + )) + self.env.process(self._deliver( + self.route_table[taken.target_pe], data_tok, + )) + if isinstance(not_taken, FrameDest): + trig_tok = self._make_token_from_dest(not_taken, 0) + self.output_log.append(trig_tok) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=trig_tok, + )) + self.env.process(self._deliver( + self.route_table[not_taken.target_pe], trig_tok, + )) + else: + out_r = self._make_token_from_dest(dest_r, result) + self.output_log.append(out_r) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=out_r, + )) + self.env.process(self._deliver( + self.route_table[dest_r.target_pe], out_r, + )) + + def _emit_change_tag( + self, + inst: Instruction, + result: int, + left: int, + ) -> None: + """CHANGE_TAG output: unpack left operand (flit 1) to get destination.""" + dest = unpack_flit1(left) + out_token = self._make_token_from_dest(dest, result) + self.output_log.append(out_token) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=out_token, + )) + self.env.process(self._deliver(self.route_table[dest.target_pe], out_token)) + + def _emit_sink(self, inst: Instruction, result: int, frame_id: int) -> None: + """SINK output: write result to frame slot, emit no token.""" + self.frames[frame_id][inst.fref] = result + self._on_event(FrameSlotWritten( + time=self.env.now, component=self._component, + frame_id=frame_id, slot=inst.fref, value=result, + )) + + def _build_and_emit_sm_new( + self, + inst: Instruction, + left: int, + right: Optional[int], + act_id: int, + frame_id: int, + ) -> None: + """Build and emit SM token. + + Return route is a FrameDest stored at inst.fref + (1 if has_const else 0). + SM target comes from frame[fref] (if has_const) or from left operand. + """ + ret_slot = inst.fref + (1 if inst.has_const else 0) + ret_dest = self.frames[frame_id][ret_slot] if inst.dest_count > 0 else None + + # Build return CMToken from FrameDest if return route exists + ret_token = None + if isinstance(ret_dest, FrameDest): + ret_token = self._make_token_from_dest(ret_dest, 0) + + # Determine SM target source + if inst.has_const: + target_packed = self.frames[frame_id][inst.fref] + else: + target_packed = left sm_token = SMToken( - target=inst.sm_id, - addr=cell_addr, - op=inst.op, - flags=left if inst.op == MemOp.CMP_SW and right is not None else None, - data=data, - ret=ret, + target=(target_packed >> 8) & 0xFF, + addr=target_packed & 0xFF, + op=inst.opcode, + flags=right if right is not None else None, + data=right if inst.has_const else left, + ret=ret_token, ) self.output_log.append(sm_token) - self._on_event(Emitted(time=self.env.now, component=self._component, token=sm_token)) - self.env.process(self._deliver(self.sm_routes[inst.sm_id], sm_token)) + self._on_event(Emitted( + time=self.env.now, component=self._component, token=sm_token, + )) + self.env.process(self._deliver(self.sm_routes[sm_token.target], sm_token)) - def _deliver(self, store: simpy.Store, token): - yield self.env.timeout(1) # 1-cycle network latency + def _make_token_from_dest(self, dest: FrameDest, data: int) -> CMToken: + """Construct CMToken from FrameDest and data.""" + match dest.token_kind: + case TokenKind.DYADIC: + return DyadToken( + target=dest.target_pe, offset=dest.offset, + act_id=dest.act_id, data=data, + port=dest.port, + ) + case TokenKind.MONADIC: + return MonadToken( + target=dest.target_pe, offset=dest.offset, + act_id=dest.act_id, data=data, inline=False, + ) + case TokenKind.INLINE: + return MonadToken( + target=dest.target_pe, offset=dest.offset, + act_id=dest.act_id, data=data, inline=True, + ) + + def _deliver(self, store: simpy.Store, token: PEToken) -> None: + """Spawn delivery process: 1 cycle delay, then put token.""" + yield self.env.timeout(1) yield store.put(token) - - def _output_mode(self, inst: ALUInst, bool_out: bool) -> OutputMode: - if inst.op == RoutingOp.FREE_CTX: - return OutputMode.SUPPRESS - if inst.op == RoutingOp.GATE and not bool_out: - return OutputMode.SUPPRESS - if inst.dest_l is None: - return OutputMode.SUPPRESS - if inst.dest_r is None: - return OutputMode.SINGLE - if isinstance(inst.op, RoutingOp) and inst.op in ( - RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, - ): - return OutputMode.SWITCH - return OutputMode.DUAL - - def _make_output_token(self, dest: Addr, data: int, ctx: int) -> DyadToken: - return DyadToken( - target=dest.pe, - offset=dest.a, - ctx=ctx, - data=data, - port=dest.port, - gen=self.gen_counters[ctx], - wide=False, - ) diff --git a/emu/types.py b/emu/types.py --- a/emu/types.py +++ b/emu/types.py @@ -15,8 +15,8 @@ @dataclass(frozen=True) class PEConfig: - pe_id: int - iram: dict[int, Instruction] + pe_id: int = 0 + iram: dict[int, Instruction] | None = None frame_count: int = 8 frame_slots: int = 64 matchable_offsets: int = 8 diff --git a/tests/test_alu.py b/tests/test_alu.py --- a/tests/test_alu.py +++ b/tests/test_alu.py @@ -82,16 +82,16 @@ @given(uint16, shift_amount) def test_shift_left(self, a, shift): - """AC2.3: SHIFT_L produces correct left shift results.""" - result, bool_out = execute(ArithOp.SHIFT_L, a, None, shift) + """AC2.3: SHL produces correct left shift results.""" + result, bool_out = execute(ArithOp.SHL, a, None, shift) expected = (a << shift) & 0xFFFF assert result == expected assert bool_out is False @given(uint16, shift_amount) def test_shift_right(self, a, shift): - """AC2.3: SHIFT_R produces correct right shift results.""" - result, bool_out = execute(ArithOp.SHIFT_R, a, None, shift) + """AC2.3: SHR produces correct right shift results.""" + result, bool_out = execute(ArithOp.SHR, a, None, shift) expected = a >> shift assert result == expected assert bool_out is False @@ -99,8 +99,8 @@ @given(uint16, shift_amount) @example(0x8000, 1) # Edge case: sign extension test def test_arithmetic_shift_right(self, a, shift): - """AC2.3: ASHFT_R sign-extends from bit 15.""" - result, bool_out = execute(ArithOp.ASHFT_R, a, None, shift) + """AC2.3: ASR sign-extends from bit 15.""" + result, bool_out = execute(ArithOp.ASR, a, None, shift) signed = to_signed(a) expected = (signed >> shift) & 0xFFFF assert result == expected @@ -308,8 +308,8 @@ @given(uint16) def test_free_returns_zero(self, a): - """AC2.7: FREE returns 0.""" - result, bool_out = execute(RoutingOp.FREE_CTX, a, None, None) + """AC2.7: FREE_FRAME returns 0.""" + result, bool_out = execute(RoutingOp.FREE_FRAME, a, None, None) assert result == 0 assert bool_out is False diff --git a/tests/test_pe_frames.py b/tests/test_pe_frames.py new file mode 100644 --- /dev/null +++ b/tests/test_pe_frames.py @@ -0,0 +1,686 @@ +""" +Frame-based PE rewrite tests. + +Verifies pe-frame-redesign.AC3 and pe-frame-redesign.AC1.6: +- AC3.1: Frame count/slots/matchable_offsets are configurable +- AC3.2: Pipeline order is IFETCH → act_id resolution → MATCH/FRAME → EXECUTE → EMIT +- AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM +- AC3.4: INHERIT output reads FrameDest from frame and routes token +- AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination +- AC3.6: SINK writes result to frame slot, emits no token +- AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind +- AC3.8: ALLOC/FREE frame control, FREE_FRAME opcode, ALLOC_REMOTE remote allocation +- AC3.9: PELocalWriteToken with is_dest=True decodes FrameDest +- AC3.10: Pipeline timing: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths +- AC1.6: Invalid act_id emits TokenRejected, doesn't crash +""" + +import pytest +import simpy + +from cm_inst import ( + ArithOp, FrameDest, FrameOp, Instruction, LogicOp, MemOp, + OutputStyle, Port, RoutingOp, TokenKind, +) +from encoding import pack_flit1, unpack_flit1, pack_instruction, unpack_instruction +from emu.events import ( + Emitted, Executed, FrameAllocated, FrameFreed, FrameSlotWritten, + Matched, TokenReceived, TokenRejected, +) +from emu.pe import ProcessingElement +from emu.types import PEConfig +from tokens import ( + DyadToken, FrameControlToken, MonadToken, PELocalWriteToken, +) + + +def inject_and_run(env, pe, token): + """Helper: inject token and run simulation.""" + def _put(): + yield pe.input_store.put(token) + env.process(_put()) + env.run() + + +class TestFrameConfiguration: + """AC3.1: PE constructor accepts frame_count, frame_slots, matchable_offsets.""" + + def test_constructor_default_params(self): + env = simpy.Environment() + pe = ProcessingElement( + env=env, + pe_id=0, + config=PEConfig(), + ) + # Default config should have frame_count, frame_slots, matchable_offsets + assert pe.frame_count > 0 + assert pe.frame_slots > 0 + assert pe.matchable_offsets > 0 + + def test_constructor_custom_params(self): + env = simpy.Environment() + config = PEConfig( + frame_count=4, + frame_slots=32, + matchable_offsets=4, + ) + pe = ProcessingElement( + env=env, + pe_id=1, + config=config, + ) + assert pe.frame_count == 4 + assert pe.frame_slots == 32 + assert pe.matchable_offsets == 4 + + +class TestFrameAllocationAndFree: + """AC3.8: Frame allocation (ALLOC) and deallocation (FREE) via FrameControlToken.""" + + def test_alloc_frame_control_token(self): + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Inject FrameControlToken(ALLOC) for act_id=0 + fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + + # Should have TokenReceived and FrameAllocated events + token_received = [e for e in events if isinstance(e, TokenReceived)] + frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] + assert len(token_received) > 0 + assert len(frame_allocated) > 0 + assert pe.tag_store[0] in range(pe.frame_count) + + def test_free_frame_control_token(self): + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Allocate first + fct_alloc = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct_alloc) + + frame_id = pe.tag_store[0] + + # Now deallocate + fct_free = FrameControlToken(target=0, act_id=0, op=FrameOp.FREE, payload=0) + inject_and_run(env, pe, fct_free) + + # Should have FrameFreed event and tag_store should be cleared + frame_freed = [e for e in events if isinstance(e, FrameFreed)] + assert len(frame_freed) > 0 + assert 0 not in pe.tag_store + assert frame_id in pe.free_frames + + +class TestDyadicMatching: + """AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM.""" + + def test_dyadic_token_pair_matching(self): + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, matchable_offsets=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Set up: allocate frame for act_id=0 + fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + + # Set up: install dyadic instruction at offset 0 + # Mode 0: no const, dest_count=1 + inst = Instruction( + opcode=ArithOp.ADD, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=1, + wide=False, + fref=0, + ) + pe.iram[0] = inst + + # Set up: write destination FrameDest to frame slot 0 + dest = FrameDest( + target_pe=0, + offset=1, + act_id=0, + port=Port.L, + token_kind=TokenKind.DYADIC, + ) + pe.frames[pe.tag_store[0]][0] = dest + + # Inject first dyadic token (port=L, data=5) + tok1 = DyadToken( + target=0, + offset=0, + act_id=0, + data=5, + port=Port.L, + ) + inject_and_run(env, pe, tok1) + + # Should have TokenReceived, no match yet (waiting for partner) + token_received = [e for e in events if isinstance(e, TokenReceived)] + matched = [e for e in events if isinstance(e, Matched)] + assert len(token_received) >= 1 + assert len(matched) == 0 + + # Inject second dyadic token (port=R, data=3) + tok2 = DyadToken( + target=0, + offset=0, + act_id=0, + data=3, + port=Port.R, + ) + inject_and_run(env, pe, tok2) + + # Should now have Matched event + matched = [e for e in events if isinstance(e, Matched)] + assert len(matched) > 0 + m = matched[0] + assert m.left == 5 + assert m.right == 3 + + +class TestInheritOutput: + """AC3.4: INHERIT output reads FrameDest from frame and constructs token.""" + + def test_inherit_single_dest(self): + 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=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[0] + + # Set up instruction: mode 0 (no const, dest_count=1), fref=8 + inst = Instruction( + opcode=ArithOp.INC, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=1, + wide=False, + fref=8, + ) + pe.iram[2] = inst + + # Set destination at frame[8] + dest = FrameDest( + target_pe=0, + offset=5, + act_id=1, + port=Port.L, + token_kind=TokenKind.MONADIC, + ) + pe.frames[frame_id][8] = dest + + # Wire route table + pe.route_table[0] = simpy.Store(env) + + # Inject monadic token + tok = MonadToken( + target=0, + offset=2, + act_id=0, + data=10, + inline=False, + ) + inject_and_run(env, pe, tok) + + # Should have Emitted event with output token routed to target_pe=0, offset=5, act_id=1 + emitted = [e for e in events if isinstance(e, Emitted)] + assert len(emitted) > 0 + out_token = emitted[0].token + assert out_token.target == 0 + assert out_token.offset == 5 + assert out_token.act_id == 1 + + +class TestChangeTagOutput: + """AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination.""" + + def test_change_tag_output(self): + 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=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + + # Set up instruction: CHANGE_TAG output, mode 4 (no const, dest_count=1) + inst = Instruction( + opcode=ArithOp.ADD, + output=OutputStyle.CHANGE_TAG, + has_const=False, + dest_count=1, + wide=False, + fref=0, + ) + pe.iram[3] = inst + + # Wire route table + pe.route_table[1] = simpy.Store(env) + + # Construct a packed flit 1 for destination (pe=1, offset=7, act_id=2, port=R, kind=DYADIC) + dest = FrameDest( + target_pe=1, + offset=7, + act_id=2, + port=Port.R, + token_kind=TokenKind.DYADIC, + ) + flit1 = pack_flit1(dest) + + # Inject dyadic token pair: first (L) carries the flit1 as left operand + tok_l = DyadToken( + target=0, + offset=3, + act_id=0, + data=flit1, # packed flit 1 + port=Port.L, + ) + inject_and_run(env, pe, tok_l) + + # Inject second (R) with some data value + tok_r = DyadToken( + target=0, + offset=3, + act_id=0, + data=100, + port=Port.R, + ) + inject_and_run(env, pe, tok_r) + + # Should emit token with target=1, offset=7, act_id=2 + emitted = [e for e in events if isinstance(e, Emitted)] + assert len(emitted) > 0 + out_token = emitted[-1].token # Last emitted token + assert out_token.target == 1 + assert out_token.offset == 7 + assert out_token.act_id == 2 + + +class TestSinkOutput: + """AC3.6: SINK output writes result to frame slot, emits no token.""" + + def test_sink_writes_to_frame(self): + 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=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[0] + + # Set up instruction: SINK output, mode 6 (no const, dest_count=0), fref=10 + inst = Instruction( + opcode=ArithOp.INC, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=10, + ) + pe.iram[4] = inst + + # Inject monadic token + tok = MonadToken( + target=0, + offset=4, + act_id=0, + data=42, + inline=False, + ) + inject_and_run(env, pe, tok) + + # Should have FrameSlotWritten event and NO Emitted event + slot_written = [e for e in events if isinstance(e, FrameSlotWritten)] + emitted = [e for e in events if isinstance(e, Emitted)] + assert len(slot_written) > 0 + assert slot_written[0].slot == 10 + assert slot_written[0].value == 43 # INC(42) = 43 + assert len(emitted) == 0 # SINK doesn't emit + + +class TestExtractTag: + """AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind.""" + + def test_extract_tag_monadic(self): + 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=2, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + + # 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[5] = inst + + # Set output destination at frame[0] + dest = FrameDest( + target_pe=0, + offset=10, + act_id=0, + port=Port.L, + token_kind=TokenKind.MONADIC, + ) + pe.frames[pe.tag_store[0]][0] = dest + + # Wire route table + pe.route_table[0] = simpy.Store(env) + + # Inject monadic token at offset 5, act_id 0 + tok = MonadToken( + target=2, + offset=5, + act_id=0, + data=999, # ignored by EXTRACT_TAG + inline=False, + ) + inject_and_run(env, pe, tok) + + # Should have Executed event showing EXTRACT_TAG + executed = [e for e in events if isinstance(e, Executed)] + assert len(executed) > 0 + assert executed[0].op == RoutingOp.EXTRACT_TAG + + # Output should be a packed flit 1 for (pe=2, offset=5, act_id=0) + emitted = [e for e in events if isinstance(e, Emitted)] + assert len(emitted) > 0 + out_token = emitted[0].token + # The result should encode (pe=2, offset=5, act_id=0, port=?, kind=?) + # Unpack and verify + flit1_val = out_token.data + unpacked = unpack_flit1(flit1_val) + assert unpacked.target_pe == 2 + assert unpacked.offset == 5 + assert unpacked.act_id == 0 + + +class TestAllocRemote: + """AC3.8: ALLOC_REMOTE reads target PE and act_id from frame, sends FrameControlToken.""" + + def test_alloc_remote(self): + env = simpy.Environment() + events = [] + pe_events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Create a second PE to receive ALLOC + config1 = PEConfig(frame_count=4, on_event=pe_events.append) + pe1 = ProcessingElement( + env=env, + pe_id=1, + config=config1, + ) + + # Wire route_table for PE0 to reach PE1 + pe.route_table[1] = pe1.input_store + + # Allocate frame for act_id=0 on PE0 + fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + + # Set up ALLOC_REMOTE instruction, mode 6 (no const, dest_count=0), fref=8 + inst = Instruction( + opcode=RoutingOp.ALLOC_REMOTE, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=8, + ) + pe.iram[6] = inst + + # Write target PE and target act_id to frame slots 8 and 9 + frame_id = pe.tag_store[0] + pe.frames[frame_id][8] = 1 # target PE + pe.frames[frame_id][9] = 2 # target act_id + + # Inject monadic token + tok = MonadToken( + target=0, + offset=6, + act_id=0, + data=0, + inline=False, + ) + inject_and_run(env, pe, tok) + + # PE1 should have received a FrameControlToken(ALLOC) for act_id=2 + frame_allocated = [e for e in pe_events if isinstance(e, FrameAllocated)] + assert len(frame_allocated) > 0 + assert frame_allocated[0].act_id == 2 + + +class TestFreeFrameOpcode: + """AC3.8: FREE_FRAME opcode deallocates frame, clears tag_store, no output.""" + + def test_free_frame_opcode(self): + 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=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[0] + + # Set up FREE_FRAME instruction + inst = Instruction( + opcode=RoutingOp.FREE_FRAME, + output=OutputStyle.SINK, + has_const=False, + dest_count=0, + wide=False, + fref=0, + ) + pe.iram[7] = inst + + # Inject monadic token + tok = MonadToken( + target=0, + offset=7, + act_id=0, + data=0, + inline=False, + ) + inject_and_run(env, pe, tok) + + # Should have FrameFreed event + frame_freed = [e for e in events if isinstance(e, FrameFreed)] + assert len(frame_freed) > 0 + assert frame_freed[0].frame_id == frame_id + + # tag_store should be cleared + assert 0 not in pe.tag_store + + # frame should be in free_frames + assert frame_id in pe.free_frames + + # Should have NO Emitted event (FREE_FRAME suppresses) + emitted = [e for e in events if isinstance(e, Emitted)] + assert len(emitted) == 0 + + +class TestPELocalWriteToken: + """AC3.9: PELocalWriteToken with is_dest=True decodes data to FrameDest.""" + + def test_local_write_iram(self): + env = simpy.Environment() + config = PEConfig() + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Write instruction to IRAM at slot 10 + inst = Instruction( + opcode=ArithOp.ADD, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=1, + wide=False, + fref=0, + ) + inst_word = pack_instruction(inst) + + write_tok = PELocalWriteToken( + target=0, + act_id=0, + region=0, # IRAM + slot=10, + data=inst_word, + is_dest=False, + ) + inject_and_run(env, pe, write_tok) + + # Should have written instruction to pe.iram[10] + assert 10 in pe.iram + # Unpack and verify + loaded_inst = pe.iram[10] + assert isinstance(loaded_inst, Instruction) + + def test_local_write_frame_dest(self): + env = simpy.Environment() + config = PEConfig(frame_count=4) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Allocate frame + fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) + inject_and_run(env, pe, fct) + frame_id = pe.tag_store[0] + + # Write FrameDest to frame slot 15, is_dest=True + dest = FrameDest( + target_pe=1, + offset=8, + act_id=3, + port=Port.R, + token_kind=TokenKind.DYADIC, + ) + flit1 = pack_flit1(dest) + + write_tok = PELocalWriteToken( + target=0, + act_id=0, + region=1, # Frame + slot=15, + data=flit1, + is_dest=True, + ) + inject_and_run(env, pe, write_tok) + + # Frame slot 15 should contain a FrameDest object + slot_val = pe.frames[frame_id][15] + assert isinstance(slot_val, FrameDest) + assert slot_val.target_pe == 1 + assert slot_val.offset == 8 + assert slot_val.act_id == 3 + + +class TestInvalidActId: + """AC1.6: Invalid act_id emits TokenRejected, doesn't crash.""" + + def test_invalid_act_id_rejected(self): + env = simpy.Environment() + events = [] + config = PEConfig(frame_count=4, on_event=events.append) + pe = ProcessingElement( + env=env, + pe_id=0, + config=config, + ) + + # Set up instruction at offset 0 (so we get past IFETCH) + inst = Instruction( + opcode=ArithOp.INC, + output=OutputStyle.INHERIT, + has_const=False, + dest_count=1, + wide=False, + fref=0, + ) + pe.iram[0] = inst + + # Inject token with act_id not in tag_store + tok = MonadToken( + target=0, + offset=0, + act_id=99, # not allocated + data=0, + inline=False, + ) + inject_and_run(env, pe, tok) + + # Should have TokenRejected event + rejected = [e for e in events if isinstance(e, TokenRejected)] + assert len(rejected) > 0 + assert rejected[0].token == tok + + # Should not crash + assert True