From 9325c1951ca81a9bcfab649cdb31100982ad442b Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Mon, 8 Apr 2024 10:00:04 +0200 Subject: [PATCH 01/19] implement pipeline stall for data hazards --- .../uarch/riscv/pipeline.py | 64 +++++++++++++++++++ .../uarch/riscv/pipeline_registers.py | 3 +- .../uarch/riscv/riscv_performance_metrics.py | 2 + architecture_simulator/uarch/riscv/stages.py | 17 +++-- tests/test_riscv_pipeline.py | 15 +++++ tests/test_riscv_simulation.py | 6 +- 6 files changed, 98 insertions(+), 9 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index ae50ea50..81006630 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -42,6 +42,11 @@ def __init__( PipelineRegister() ] * self.num_stages + # if != None: [stage to cause stall, remaining duration] + self.stalled: list[int] | None = None + # holds the old content of the first stage, since the first stage must not be rerun + self.stalled_pipeline_reg: PipelineRegister | None = None + def step(self): """the pipeline step method, this is the central part of the pipeline! Every time it is called, it does one whole step of the pipeline, and every stage gets executed once in their execution ordering @@ -50,6 +55,34 @@ def step(self): next_pipeline_registers = [None] * self.num_stages for index in self.execution_ordering: try: + if self.stalled is not None: + if index == 0: # first stage must not be recomputed when stalling + next_pipeline_registers[0] = self.pipeline_registers[0] + continue + elif ( + index == 1 + ): # second stage must get the old content of the pipelineregister of the first stage when stalling + tmp = self.pipeline_registers[0] + self.pipeline_registers[0] = self.stalled_pipeline_reg + next_pipeline_registers[index] = self.stages[index].behavior( + pipeline_registers=self.pipeline_registers, + index_of_own_input_register=(index - 1), + state=self.state, + ) + self.pipeline_registers[0] = tmp + continue + elif ( + index == self.stalled[0] + 1 + ): # first stage after the stalled stages must get and empty PipelineRegister while stalling + tmp = self.pipeline_registers[self.stalled[0]] + self.pipeline_registers[self.stalled[0]] = PipelineRegister() + next_pipeline_registers[index] = self.stages[index].behavior( + pipeline_registers=self.pipeline_registers, + index_of_own_input_register=(index - 1), + state=self.state, + ) + self.pipeline_registers[self.stalled[0]] = tmp + continue next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), @@ -68,8 +101,33 @@ def step(self): ) else: raise + + # Check if a stage has produced a stall signal + for index, pipeline_register in reversed( + list(enumerate(next_pipeline_registers)) + ): + if ( + pipeline_register is not None + and pipeline_register.stall_signal is not None + and (self.stalled is None or index > self.stalled[0]) + ): + self.stalled = [index, pipeline_register.stall_signal.duration + 1] + self.state.performance_metrics.stalls += 1 + break + + # Keep the old value of the first pipeline register + if self.stalled and self.stalled_pipeline_reg is None: + self.stalled_pipeline_reg = self.pipeline_registers[0] + self.pipeline_registers = next_pipeline_registers + # Check if done stalling + if self.stalled is not None: + self.stalled[1] -= 1 + if self.stalled[1] == 0: + self.stalled = None + self.stalled_pipeline_reg = None + # if one of the stages wants to flush, do so (starting from the back makes sense) for index, pipeline_register in reversed( list(enumerate(self.pipeline_registers)) @@ -84,6 +142,12 @@ def step(self): PipelineRegister() ] * num_to_flush self.state.program_counter = flush_signal.address + + # Unstall stages that have been flushed + if self.stalled is not None and self.stalled[0] < num_to_flush: + self.stalled = None + self.stalled_pipeline_reg = None + break # break since we don't care about the previous stages def is_empty(self) -> bool: diff --git a/architecture_simulator/uarch/riscv/pipeline_registers.py b/architecture_simulator/uarch/riscv/pipeline_registers.py index 9f1b8770..fcd69dcf 100644 --- a/architecture_simulator/uarch/riscv/pipeline_registers.py +++ b/architecture_simulator/uarch/riscv/pipeline_registers.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from architecture_simulator.isa.riscv.instruction_types import RiscvInstruction - from .stages import FlushSignal + from .stages import FlushSignal, StallSignal @dataclass @@ -19,6 +19,7 @@ class PipelineRegister: instruction: RiscvInstruction = field(default_factory=EmptyInstruction) address_of_instruction: Optional[int] = None flush_signal: Optional[FlushSignal] = None + stall_signal: Optional[StallSignal] = None abbreviation = "Single" diff --git a/architecture_simulator/uarch/riscv/riscv_performance_metrics.py b/architecture_simulator/uarch/riscv/riscv_performance_metrics.py index 101fc26f..e4e76415 100644 --- a/architecture_simulator/uarch/riscv/riscv_performance_metrics.py +++ b/architecture_simulator/uarch/riscv/riscv_performance_metrics.py @@ -8,6 +8,7 @@ class RiscvPerformanceMetrics(PerformanceMetrics): branch_count: int = 0 procedure_count: int = 0 flushes: int = 0 + stalls: int = 0 cycles: int = 0 def __repr__(self) -> str: @@ -25,6 +26,7 @@ def __repr__(self) -> str: representation += f"branches: {self.branch_count}\n" representation += f"procedures: {self.procedure_count}\n" representation += f"cycles: {self.cycles}\n" + representation += f"stalls: {self.stalls}\n" representation += f"flushes: {self.flushes}\n" if not self.instruction_count == 0: representation += f"cycles per instruction: {(self.cycles / self.instruction_count):.2f}\n" diff --git a/architecture_simulator/uarch/riscv/stages.py b/architecture_simulator/uarch/riscv/stages.py index 8fd157c7..856457cb 100644 --- a/architecture_simulator/uarch/riscv/stages.py +++ b/architecture_simulator/uarch/riscv/stages.py @@ -153,7 +153,7 @@ def behavior( write_register = pipeline_register.instruction.get_write_register() # Data Hazard Detection - flush_signal = None + stall_signal = None if self.detect_data_hazards: # Put all the write registers of later stages, that are not done ahead of this stage into a list write_registers_of_later_stages = [ @@ -168,10 +168,7 @@ def behavior( continue if register_read_addr_1 == register or register_read_addr_2 == register: assert pipeline_register.address_of_instruction is not None - flush_signal = FlushSignal( - inclusive=True, - address=pipeline_register.address_of_instruction, - ) + stall_signal = StallSignal(2) break # gets the control unit signals that are generated in the ID stage @@ -186,7 +183,7 @@ def behavior( write_register=write_register, control_unit_signals=control_unit_signals, branch_prediction=pipeline_register.branch_prediction, - flush_signal=flush_signal, + stall_signal=stall_signal, pc_plus_instruction_length=pipeline_register.pc_plus_instruction_length, address_of_instruction=pipeline_register.address_of_instruction, ) @@ -571,3 +568,11 @@ class FlushSignal: inclusive: bool # address to return to address: int + + +@dataclass +class StallSignal: + """A signal that this stage and all previous stages should be stalled for a duration of cycles""" + + # how many cycles to stall + duration: int diff --git a/tests/test_riscv_pipeline.py b/tests/test_riscv_pipeline.py index 7ad52552..4a922067 100644 --- a/tests/test_riscv_pipeline.py +++ b/tests/test_riscv_pipeline.py @@ -1381,3 +1381,18 @@ def test_fix_too_many_flushes(self): self.assert_steps(simulation=simulation, steps=9) self.assertEqual(simulation.state.register_file.registers[2], 2) + + def test_stall(self): + program = """ + addi x1, x0, 11 + addi x2, x1, 22 + """ + + sim = RiscvSimulation(mode="five_stage_pipeline") + + sim.load_program(program) + sim.run() + self.assertEqual(sim.state.register_file.registers[2], 33) + self.assertEqual(sim.state.performance_metrics.cycles, 8) + self.assertEqual(sim.state.performance_metrics.stalls, 1) + self.assertEqual(sim.state.performance_metrics.flushes, 0) diff --git a/tests/test_riscv_simulation.py b/tests/test_riscv_simulation.py index f69fd8f6..a775fd88 100644 --- a/tests/test_riscv_simulation.py +++ b/tests/test_riscv_simulation.py @@ -269,7 +269,8 @@ def test_five_stage_performance_metrics_2(self): """ simulation.load_program(program=programm) simulation.run() - self.assertEqual(simulation.state.performance_metrics.flushes, 2) + self.assertEqual(simulation.state.performance_metrics.flushes, 1) + self.assertEqual(simulation.state.performance_metrics.stalls, 1) self.assertEqual(simulation.state.performance_metrics.cycles, 12) def test_off_by_one_fix(self): @@ -332,7 +333,8 @@ def test_five_stage_performance_metrics_3(self): self.assertEqual(simulation.state.register_file.registers[3], 160) self.assertEqual(simulation.state.performance_metrics.instruction_count, 32) self.assertEqual(simulation.state.performance_metrics.branch_count, 9) - self.assertEqual(simulation.state.performance_metrics.flushes, 20) + self.assertEqual(simulation.state.performance_metrics.flushes, 9) + self.assertEqual(simulation.state.performance_metrics.stalls, 11) def test_has_started(self): sim = RiscvSimulation() From 3bfa5caaf682467dff8c288eabf707f2967ea6a1 Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Mon, 8 Apr 2024 12:09:09 +0200 Subject: [PATCH 02/19] add stalls for ecall --- .../uarch/riscv/pipeline.py | 35 +++++++++++-------- .../uarch/riscv/pipeline_registers.py | 3 ++ architecture_simulator/uarch/riscv/stages.py | 18 +++++----- tests/test_ecalls.py | 24 ++++++++++++- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index 81006630..4564be5d 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -45,7 +45,7 @@ def __init__( # if != None: [stage to cause stall, remaining duration] self.stalled: list[int] | None = None # holds the old content of the first stage, since the first stage must not be rerun - self.stalled_pipeline_reg: PipelineRegister | None = None + self.stalled_pipeline_regs: list[PipelineRegister] | None = None def step(self): """the pipeline step method, this is the central part of the pipeline! Every time it is called, it does one @@ -60,28 +60,30 @@ def step(self): next_pipeline_registers[0] = self.pipeline_registers[0] continue elif ( - index == 1 - ): # second stage must get the old content of the pipelineregister of the first stage when stalling - tmp = self.pipeline_registers[0] - self.pipeline_registers[0] = self.stalled_pipeline_reg + index == self.stalled[0] + 1 + ): # first stage after the stalled stages must get an empty PipelineRegister while stalling + tmp = self.pipeline_registers[self.stalled[0]] + self.pipeline_registers[self.stalled[0]] = PipelineRegister() next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) - self.pipeline_registers[0] = tmp + self.pipeline_registers[self.stalled[0]] = tmp continue elif ( - index == self.stalled[0] + 1 - ): # first stage after the stalled stages must get and empty PipelineRegister while stalling - tmp = self.pipeline_registers[self.stalled[0]] - self.pipeline_registers[self.stalled[0]] = PipelineRegister() + index <= self.stalled[0] + ): # Other stalled stages should get the old PipelineRegister values NOTE: This is fine, because the only instruction that stalls in EX is ecall and ecall does not get it´s register values from the previous PipelineRegister + tmp = self.pipeline_registers[index - 1] + self.pipeline_registers[index - 1] = self.stalled_pipeline_regs[ + index - 1 + ] next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) - self.pipeline_registers[self.stalled[0]] = tmp + self.pipeline_registers[index - 1] = tmp continue next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, @@ -116,8 +118,11 @@ def step(self): break # Keep the old value of the first pipeline register - if self.stalled and self.stalled_pipeline_reg is None: - self.stalled_pipeline_reg = self.pipeline_registers[0] + if self.stalled and self.stalled_pipeline_regs is None: + self.stalled_pipeline_regs = self.pipeline_registers[: self.stalled[0]] + + for reg in self.stalled_pipeline_regs: + reg.is_of_stalled_value = True self.pipeline_registers = next_pipeline_registers @@ -126,7 +131,7 @@ def step(self): self.stalled[1] -= 1 if self.stalled[1] == 0: self.stalled = None - self.stalled_pipeline_reg = None + self.stalled_pipeline_regs = None # if one of the stages wants to flush, do so (starting from the back makes sense) for index, pipeline_register in reversed( @@ -146,7 +151,7 @@ def step(self): # Unstall stages that have been flushed if self.stalled is not None and self.stalled[0] < num_to_flush: self.stalled = None - self.stalled_pipeline_reg = None + self.stalled_pipeline_regs = None break # break since we don't care about the previous stages diff --git a/architecture_simulator/uarch/riscv/pipeline_registers.py b/architecture_simulator/uarch/riscv/pipeline_registers.py index fcd69dcf..71dd00ea 100644 --- a/architecture_simulator/uarch/riscv/pipeline_registers.py +++ b/architecture_simulator/uarch/riscv/pipeline_registers.py @@ -22,6 +22,9 @@ class PipelineRegister: stall_signal: Optional[StallSignal] = None abbreviation = "Single" + # True, if the register is being separately preserved by the pipeline for stalling + is_of_stalled_value: bool = False + @dataclass class InstructionFetchPipelineRegister(PipelineRegister): diff --git a/architecture_simulator/uarch/riscv/stages.py b/architecture_simulator/uarch/riscv/stages.py index 856457cb..2d724f65 100644 --- a/architecture_simulator/uarch/riscv/stages.py +++ b/architecture_simulator/uarch/riscv/stages.py @@ -236,21 +236,23 @@ def behavior( ) # ECALL needs some special behavior (flush and print to output) - flush_signal = None + stall_signal = None if isinstance(pipeline_register.instruction, ECALL): - # assume that all further stages need to be empty - for other_pr in pipeline_registers[index_of_own_input_register + 1 : -1]: + # assume that all further stages need to be empty, unless this stage is already stalled and the value of the next register is only for display purposes + for other_pr in pipeline_registers[ + index_of_own_input_register + + 1 + + int(pipeline_register.is_of_stalled_value) : -1 + ]: if not isinstance(other_pr.instruction, EmptyInstruction): assert pipeline_register.address_of_instruction is not None - flush_signal = FlushSignal( - True, pipeline_register.address_of_instruction - ) + stall_signal = StallSignal(2) break - if flush_signal is None: + if stall_signal is None: pipeline_register.instruction.behavior(state) return ExecutePipelineRegister( - flush_signal=flush_signal, + stall_signal=stall_signal, instruction=pipeline_register.instruction, alu_in_1=alu_in_1, alu_in_2=alu_in_2, diff --git a/tests/test_ecalls.py b/tests/test_ecalls.py index 16e329a3..8449109d 100644 --- a/tests/test_ecalls.py +++ b/tests/test_ecalls.py @@ -93,4 +93,26 @@ def test_five_stage_ecalls(self): ) simulation.run() self.assertEqual(simulation.get_output(), "Kaesekuchen ist toll.") - self.assertEqual(simulation.get_performance_metrics().cycles, 13) + self.assertEqual(simulation.get_performance_metrics().cycles, 12) + + def test_five_stage_ecalls_2(self): + simulation = RiscvSimulation(mode="five_stage_pipeline") + simulation.load_program( + """ +.data + kaesekuchen: .string "Kaesekuchen ist toll." +.text +la a0, kaesekuchen +li a7, 2 +beq zero, zero, label +nop +nop +nop +label: +add a7, a7, a7 +ecall +""" + ) + simulation.run() + self.assertEqual(simulation.get_output(), "Kaesekuchen ist toll.") + # NOTE: This works, because the ecall reads the register values directly and not out of the pipeline registers From e1402be76f35c1af5fb402b539fc9a413b432fa4 Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Thu, 11 Apr 2024 11:14:53 +0200 Subject: [PATCH 03/19] add comments and tests for pipeline stall --- .../uarch/riscv/pipeline.py | 12 +++++----- tests/test_riscv_pipeline.py | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index 4564be5d..e2a86ef8 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -42,9 +42,9 @@ def __init__( PipelineRegister() ] * self.num_stages - # if != None: [stage to cause stall, remaining duration] + # if != None: [index of stage to cause stall, remaining duration of stall] self.stalled: list[int] | None = None - # holds the old content of the first stage, since the first stage must not be rerun + # holds the old contents of pipeline registers that are used as input for stalled stages self.stalled_pipeline_regs: list[PipelineRegister] | None = None def step(self): @@ -73,7 +73,7 @@ def step(self): continue elif ( index <= self.stalled[0] - ): # Other stalled stages should get the old PipelineRegister values NOTE: This is fine, because the only instruction that stalls in EX is ecall and ecall does not get it´s register values from the previous PipelineRegister + ): # other stalled stages should get the old PipelineRegister values stored in stalled_pipeline_regs tmp = self.pipeline_registers[index - 1] self.pipeline_registers[index - 1] = self.stalled_pipeline_regs[ index - 1 @@ -85,11 +85,13 @@ def step(self): ) self.pipeline_registers[index - 1] = tmp continue + next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) + except Exception as e: if index - 1 >= 0: raise InstructionExecutionException( @@ -104,7 +106,7 @@ def step(self): else: raise - # Check if a stage has produced a stall signal + # Check if a stage has produced a meaningfull stall signal for index, pipeline_register in reversed( list(enumerate(next_pipeline_registers)) ): @@ -117,7 +119,7 @@ def step(self): self.state.performance_metrics.stalls += 1 break - # Keep the old value of the first pipeline register + # keep PipelineRegister values in stalled_pipeline_regs if self.stalled and self.stalled_pipeline_regs is None: self.stalled_pipeline_regs = self.pipeline_registers[: self.stalled[0]] diff --git a/tests/test_riscv_pipeline.py b/tests/test_riscv_pipeline.py index 4a922067..881a5b18 100644 --- a/tests/test_riscv_pipeline.py +++ b/tests/test_riscv_pipeline.py @@ -1396,3 +1396,25 @@ def test_stall(self): self.assertEqual(sim.state.performance_metrics.cycles, 8) self.assertEqual(sim.state.performance_metrics.stalls, 1) self.assertEqual(sim.state.performance_metrics.flushes, 0) + + def test_stall_2(self): + program = """ + add x0, x0, x0 + add x0, x0, x0 + addi x1, x0, 11 + addi x2, x1, 3 + addi x3, x1, 4 + addi x4, x2, 100 + """ + + sim = RiscvSimulation(mode="five_stage_pipeline") + + sim.load_program(program) + sim.run() + self.assertEqual(sim.state.register_file.registers[1], 11) + self.assertEqual(sim.state.register_file.registers[2], 14) + self.assertEqual(sim.state.register_file.registers[3], 15) + self.assertEqual(sim.state.register_file.registers[4], 114) + self.assertEqual(sim.state.performance_metrics.cycles, 14) + self.assertEqual(sim.state.performance_metrics.stalls, 2) + self.assertEqual(sim.state.performance_metrics.flushes, 0) From 45cb08e2fd2ffbef001aed7813bf403ba2beb58f Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Tue, 23 Apr 2024 09:59:16 +0200 Subject: [PATCH 04/19] make addr in jal, x0, addr an abs value --- architecture_simulator/isa/riscv/instruction_types.py | 5 +++-- architecture_simulator/isa/riscv/riscv_parser.py | 3 +++ architecture_simulator/isa/riscv/rv32i_instructions.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/architecture_simulator/isa/riscv/instruction_types.py b/architecture_simulator/isa/riscv/instruction_types.py index a925df66..e00a111b 100644 --- a/architecture_simulator/isa/riscv/instruction_types.py +++ b/architecture_simulator/isa/riscv/instruction_types.py @@ -416,13 +416,14 @@ def access_register_file( class JTypeInstruction(RiscvInstruction): - def __init__(self, rd: int, imm: int, **args): + def __init__(self, rd: int, imm: int, abs_addr: int, **args): super().__init__(**args) self.rd = rd self.imm = (imm & (2**20) - 1) - (imm & 2**20) # 21-bit sext + self.abs_addr = abs_addr def __repr__(self) -> str: - return f"{self.mnemonic} x{self.rd}, {self.imm}" + return f"{self.mnemonic} x{self.rd}, {self.abs_addr}" def control_unit_signals(self) -> ControlUnitSignals: return ControlUnitSignals( diff --git a/architecture_simulator/isa/riscv/riscv_parser.py b/architecture_simulator/isa/riscv/riscv_parser.py index 832a32d6..81619745 100644 --- a/architecture_simulator/isa/riscv/riscv_parser.py +++ b/architecture_simulator/isa/riscv/riscv_parser.py @@ -748,11 +748,14 @@ def _write_instructions(self) -> None: line_number=line_number, line=line, ) + if line_parsed.get("imm"): + imm_val -= address_count instructions.append( instruction_class( rd=self._convert_register_name(line_parsed.rd), imm=imm_val, + abs_addr=imm_val + address_count, ) ) elif issubclass(instruction_class, instruction_types.CSRTypeInstruction): diff --git a/architecture_simulator/isa/riscv/rv32i_instructions.py b/architecture_simulator/isa/riscv/rv32i_instructions.py index ece39060..fdb224f9 100644 --- a/architecture_simulator/isa/riscv/rv32i_instructions.py +++ b/architecture_simulator/isa/riscv/rv32i_instructions.py @@ -1217,8 +1217,8 @@ def alu_compute( class JAL(JTypeInstruction): - def __init__(self, rd: int, imm: int): - super().__init__(rd, imm, mnemonic="jal") + def __init__(self, rd: int, imm: int, abs_addr: int): + super().__init__(rd, imm, abs_addr, mnemonic="jal") def behavior( self, architectural_state: RiscvArchitecturalState From 00e7ef5f2a846dba551132571f4745596606cd8b Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Mon, 8 Apr 2024 10:00:04 +0200 Subject: [PATCH 05/19] implement pipeline stall for data hazards --- .../uarch/riscv/pipeline.py | 64 +++++++++++++++++++ .../uarch/riscv/pipeline_registers.py | 3 +- .../uarch/riscv/riscv_performance_metrics.py | 2 + architecture_simulator/uarch/riscv/stages.py | 17 +++-- tests/test_riscv_pipeline.py | 15 +++++ tests/test_riscv_simulation.py | 6 +- 6 files changed, 98 insertions(+), 9 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index ae50ea50..81006630 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -42,6 +42,11 @@ def __init__( PipelineRegister() ] * self.num_stages + # if != None: [stage to cause stall, remaining duration] + self.stalled: list[int] | None = None + # holds the old content of the first stage, since the first stage must not be rerun + self.stalled_pipeline_reg: PipelineRegister | None = None + def step(self): """the pipeline step method, this is the central part of the pipeline! Every time it is called, it does one whole step of the pipeline, and every stage gets executed once in their execution ordering @@ -50,6 +55,34 @@ def step(self): next_pipeline_registers = [None] * self.num_stages for index in self.execution_ordering: try: + if self.stalled is not None: + if index == 0: # first stage must not be recomputed when stalling + next_pipeline_registers[0] = self.pipeline_registers[0] + continue + elif ( + index == 1 + ): # second stage must get the old content of the pipelineregister of the first stage when stalling + tmp = self.pipeline_registers[0] + self.pipeline_registers[0] = self.stalled_pipeline_reg + next_pipeline_registers[index] = self.stages[index].behavior( + pipeline_registers=self.pipeline_registers, + index_of_own_input_register=(index - 1), + state=self.state, + ) + self.pipeline_registers[0] = tmp + continue + elif ( + index == self.stalled[0] + 1 + ): # first stage after the stalled stages must get and empty PipelineRegister while stalling + tmp = self.pipeline_registers[self.stalled[0]] + self.pipeline_registers[self.stalled[0]] = PipelineRegister() + next_pipeline_registers[index] = self.stages[index].behavior( + pipeline_registers=self.pipeline_registers, + index_of_own_input_register=(index - 1), + state=self.state, + ) + self.pipeline_registers[self.stalled[0]] = tmp + continue next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), @@ -68,8 +101,33 @@ def step(self): ) else: raise + + # Check if a stage has produced a stall signal + for index, pipeline_register in reversed( + list(enumerate(next_pipeline_registers)) + ): + if ( + pipeline_register is not None + and pipeline_register.stall_signal is not None + and (self.stalled is None or index > self.stalled[0]) + ): + self.stalled = [index, pipeline_register.stall_signal.duration + 1] + self.state.performance_metrics.stalls += 1 + break + + # Keep the old value of the first pipeline register + if self.stalled and self.stalled_pipeline_reg is None: + self.stalled_pipeline_reg = self.pipeline_registers[0] + self.pipeline_registers = next_pipeline_registers + # Check if done stalling + if self.stalled is not None: + self.stalled[1] -= 1 + if self.stalled[1] == 0: + self.stalled = None + self.stalled_pipeline_reg = None + # if one of the stages wants to flush, do so (starting from the back makes sense) for index, pipeline_register in reversed( list(enumerate(self.pipeline_registers)) @@ -84,6 +142,12 @@ def step(self): PipelineRegister() ] * num_to_flush self.state.program_counter = flush_signal.address + + # Unstall stages that have been flushed + if self.stalled is not None and self.stalled[0] < num_to_flush: + self.stalled = None + self.stalled_pipeline_reg = None + break # break since we don't care about the previous stages def is_empty(self) -> bool: diff --git a/architecture_simulator/uarch/riscv/pipeline_registers.py b/architecture_simulator/uarch/riscv/pipeline_registers.py index 9f1b8770..fcd69dcf 100644 --- a/architecture_simulator/uarch/riscv/pipeline_registers.py +++ b/architecture_simulator/uarch/riscv/pipeline_registers.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from architecture_simulator.isa.riscv.instruction_types import RiscvInstruction - from .stages import FlushSignal + from .stages import FlushSignal, StallSignal @dataclass @@ -19,6 +19,7 @@ class PipelineRegister: instruction: RiscvInstruction = field(default_factory=EmptyInstruction) address_of_instruction: Optional[int] = None flush_signal: Optional[FlushSignal] = None + stall_signal: Optional[StallSignal] = None abbreviation = "Single" diff --git a/architecture_simulator/uarch/riscv/riscv_performance_metrics.py b/architecture_simulator/uarch/riscv/riscv_performance_metrics.py index 101fc26f..e4e76415 100644 --- a/architecture_simulator/uarch/riscv/riscv_performance_metrics.py +++ b/architecture_simulator/uarch/riscv/riscv_performance_metrics.py @@ -8,6 +8,7 @@ class RiscvPerformanceMetrics(PerformanceMetrics): branch_count: int = 0 procedure_count: int = 0 flushes: int = 0 + stalls: int = 0 cycles: int = 0 def __repr__(self) -> str: @@ -25,6 +26,7 @@ def __repr__(self) -> str: representation += f"branches: {self.branch_count}\n" representation += f"procedures: {self.procedure_count}\n" representation += f"cycles: {self.cycles}\n" + representation += f"stalls: {self.stalls}\n" representation += f"flushes: {self.flushes}\n" if not self.instruction_count == 0: representation += f"cycles per instruction: {(self.cycles / self.instruction_count):.2f}\n" diff --git a/architecture_simulator/uarch/riscv/stages.py b/architecture_simulator/uarch/riscv/stages.py index 8fd157c7..856457cb 100644 --- a/architecture_simulator/uarch/riscv/stages.py +++ b/architecture_simulator/uarch/riscv/stages.py @@ -153,7 +153,7 @@ def behavior( write_register = pipeline_register.instruction.get_write_register() # Data Hazard Detection - flush_signal = None + stall_signal = None if self.detect_data_hazards: # Put all the write registers of later stages, that are not done ahead of this stage into a list write_registers_of_later_stages = [ @@ -168,10 +168,7 @@ def behavior( continue if register_read_addr_1 == register or register_read_addr_2 == register: assert pipeline_register.address_of_instruction is not None - flush_signal = FlushSignal( - inclusive=True, - address=pipeline_register.address_of_instruction, - ) + stall_signal = StallSignal(2) break # gets the control unit signals that are generated in the ID stage @@ -186,7 +183,7 @@ def behavior( write_register=write_register, control_unit_signals=control_unit_signals, branch_prediction=pipeline_register.branch_prediction, - flush_signal=flush_signal, + stall_signal=stall_signal, pc_plus_instruction_length=pipeline_register.pc_plus_instruction_length, address_of_instruction=pipeline_register.address_of_instruction, ) @@ -571,3 +568,11 @@ class FlushSignal: inclusive: bool # address to return to address: int + + +@dataclass +class StallSignal: + """A signal that this stage and all previous stages should be stalled for a duration of cycles""" + + # how many cycles to stall + duration: int diff --git a/tests/test_riscv_pipeline.py b/tests/test_riscv_pipeline.py index 7ad52552..4a922067 100644 --- a/tests/test_riscv_pipeline.py +++ b/tests/test_riscv_pipeline.py @@ -1381,3 +1381,18 @@ def test_fix_too_many_flushes(self): self.assert_steps(simulation=simulation, steps=9) self.assertEqual(simulation.state.register_file.registers[2], 2) + + def test_stall(self): + program = """ + addi x1, x0, 11 + addi x2, x1, 22 + """ + + sim = RiscvSimulation(mode="five_stage_pipeline") + + sim.load_program(program) + sim.run() + self.assertEqual(sim.state.register_file.registers[2], 33) + self.assertEqual(sim.state.performance_metrics.cycles, 8) + self.assertEqual(sim.state.performance_metrics.stalls, 1) + self.assertEqual(sim.state.performance_metrics.flushes, 0) diff --git a/tests/test_riscv_simulation.py b/tests/test_riscv_simulation.py index f69fd8f6..a775fd88 100644 --- a/tests/test_riscv_simulation.py +++ b/tests/test_riscv_simulation.py @@ -269,7 +269,8 @@ def test_five_stage_performance_metrics_2(self): """ simulation.load_program(program=programm) simulation.run() - self.assertEqual(simulation.state.performance_metrics.flushes, 2) + self.assertEqual(simulation.state.performance_metrics.flushes, 1) + self.assertEqual(simulation.state.performance_metrics.stalls, 1) self.assertEqual(simulation.state.performance_metrics.cycles, 12) def test_off_by_one_fix(self): @@ -332,7 +333,8 @@ def test_five_stage_performance_metrics_3(self): self.assertEqual(simulation.state.register_file.registers[3], 160) self.assertEqual(simulation.state.performance_metrics.instruction_count, 32) self.assertEqual(simulation.state.performance_metrics.branch_count, 9) - self.assertEqual(simulation.state.performance_metrics.flushes, 20) + self.assertEqual(simulation.state.performance_metrics.flushes, 9) + self.assertEqual(simulation.state.performance_metrics.stalls, 11) def test_has_started(self): sim = RiscvSimulation() From 8480352a1e4563855422e89b79eb22f735f89c3c Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Mon, 8 Apr 2024 12:09:09 +0200 Subject: [PATCH 06/19] add stalls for ecall --- .../uarch/riscv/pipeline.py | 35 +++++++++++-------- .../uarch/riscv/pipeline_registers.py | 3 ++ architecture_simulator/uarch/riscv/stages.py | 18 +++++----- tests/test_ecalls.py | 24 ++++++++++++- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index 81006630..4564be5d 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -45,7 +45,7 @@ def __init__( # if != None: [stage to cause stall, remaining duration] self.stalled: list[int] | None = None # holds the old content of the first stage, since the first stage must not be rerun - self.stalled_pipeline_reg: PipelineRegister | None = None + self.stalled_pipeline_regs: list[PipelineRegister] | None = None def step(self): """the pipeline step method, this is the central part of the pipeline! Every time it is called, it does one @@ -60,28 +60,30 @@ def step(self): next_pipeline_registers[0] = self.pipeline_registers[0] continue elif ( - index == 1 - ): # second stage must get the old content of the pipelineregister of the first stage when stalling - tmp = self.pipeline_registers[0] - self.pipeline_registers[0] = self.stalled_pipeline_reg + index == self.stalled[0] + 1 + ): # first stage after the stalled stages must get an empty PipelineRegister while stalling + tmp = self.pipeline_registers[self.stalled[0]] + self.pipeline_registers[self.stalled[0]] = PipelineRegister() next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) - self.pipeline_registers[0] = tmp + self.pipeline_registers[self.stalled[0]] = tmp continue elif ( - index == self.stalled[0] + 1 - ): # first stage after the stalled stages must get and empty PipelineRegister while stalling - tmp = self.pipeline_registers[self.stalled[0]] - self.pipeline_registers[self.stalled[0]] = PipelineRegister() + index <= self.stalled[0] + ): # Other stalled stages should get the old PipelineRegister values NOTE: This is fine, because the only instruction that stalls in EX is ecall and ecall does not get it´s register values from the previous PipelineRegister + tmp = self.pipeline_registers[index - 1] + self.pipeline_registers[index - 1] = self.stalled_pipeline_regs[ + index - 1 + ] next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) - self.pipeline_registers[self.stalled[0]] = tmp + self.pipeline_registers[index - 1] = tmp continue next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, @@ -116,8 +118,11 @@ def step(self): break # Keep the old value of the first pipeline register - if self.stalled and self.stalled_pipeline_reg is None: - self.stalled_pipeline_reg = self.pipeline_registers[0] + if self.stalled and self.stalled_pipeline_regs is None: + self.stalled_pipeline_regs = self.pipeline_registers[: self.stalled[0]] + + for reg in self.stalled_pipeline_regs: + reg.is_of_stalled_value = True self.pipeline_registers = next_pipeline_registers @@ -126,7 +131,7 @@ def step(self): self.stalled[1] -= 1 if self.stalled[1] == 0: self.stalled = None - self.stalled_pipeline_reg = None + self.stalled_pipeline_regs = None # if one of the stages wants to flush, do so (starting from the back makes sense) for index, pipeline_register in reversed( @@ -146,7 +151,7 @@ def step(self): # Unstall stages that have been flushed if self.stalled is not None and self.stalled[0] < num_to_flush: self.stalled = None - self.stalled_pipeline_reg = None + self.stalled_pipeline_regs = None break # break since we don't care about the previous stages diff --git a/architecture_simulator/uarch/riscv/pipeline_registers.py b/architecture_simulator/uarch/riscv/pipeline_registers.py index fcd69dcf..71dd00ea 100644 --- a/architecture_simulator/uarch/riscv/pipeline_registers.py +++ b/architecture_simulator/uarch/riscv/pipeline_registers.py @@ -22,6 +22,9 @@ class PipelineRegister: stall_signal: Optional[StallSignal] = None abbreviation = "Single" + # True, if the register is being separately preserved by the pipeline for stalling + is_of_stalled_value: bool = False + @dataclass class InstructionFetchPipelineRegister(PipelineRegister): diff --git a/architecture_simulator/uarch/riscv/stages.py b/architecture_simulator/uarch/riscv/stages.py index 856457cb..2d724f65 100644 --- a/architecture_simulator/uarch/riscv/stages.py +++ b/architecture_simulator/uarch/riscv/stages.py @@ -236,21 +236,23 @@ def behavior( ) # ECALL needs some special behavior (flush and print to output) - flush_signal = None + stall_signal = None if isinstance(pipeline_register.instruction, ECALL): - # assume that all further stages need to be empty - for other_pr in pipeline_registers[index_of_own_input_register + 1 : -1]: + # assume that all further stages need to be empty, unless this stage is already stalled and the value of the next register is only for display purposes + for other_pr in pipeline_registers[ + index_of_own_input_register + + 1 + + int(pipeline_register.is_of_stalled_value) : -1 + ]: if not isinstance(other_pr.instruction, EmptyInstruction): assert pipeline_register.address_of_instruction is not None - flush_signal = FlushSignal( - True, pipeline_register.address_of_instruction - ) + stall_signal = StallSignal(2) break - if flush_signal is None: + if stall_signal is None: pipeline_register.instruction.behavior(state) return ExecutePipelineRegister( - flush_signal=flush_signal, + stall_signal=stall_signal, instruction=pipeline_register.instruction, alu_in_1=alu_in_1, alu_in_2=alu_in_2, diff --git a/tests/test_ecalls.py b/tests/test_ecalls.py index 16e329a3..8449109d 100644 --- a/tests/test_ecalls.py +++ b/tests/test_ecalls.py @@ -93,4 +93,26 @@ def test_five_stage_ecalls(self): ) simulation.run() self.assertEqual(simulation.get_output(), "Kaesekuchen ist toll.") - self.assertEqual(simulation.get_performance_metrics().cycles, 13) + self.assertEqual(simulation.get_performance_metrics().cycles, 12) + + def test_five_stage_ecalls_2(self): + simulation = RiscvSimulation(mode="five_stage_pipeline") + simulation.load_program( + """ +.data + kaesekuchen: .string "Kaesekuchen ist toll." +.text +la a0, kaesekuchen +li a7, 2 +beq zero, zero, label +nop +nop +nop +label: +add a7, a7, a7 +ecall +""" + ) + simulation.run() + self.assertEqual(simulation.get_output(), "Kaesekuchen ist toll.") + # NOTE: This works, because the ecall reads the register values directly and not out of the pipeline registers From 826f9427d60bdfb3bbd87c5f27a6a8c850f098c1 Mon Sep 17 00:00:00 2001 From: MartinRoehm Date: Thu, 11 Apr 2024 11:14:53 +0200 Subject: [PATCH 07/19] add comments and tests for pipeline stall --- .../uarch/riscv/pipeline.py | 12 +++++----- tests/test_riscv_pipeline.py | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/architecture_simulator/uarch/riscv/pipeline.py b/architecture_simulator/uarch/riscv/pipeline.py index 4564be5d..e2a86ef8 100644 --- a/architecture_simulator/uarch/riscv/pipeline.py +++ b/architecture_simulator/uarch/riscv/pipeline.py @@ -42,9 +42,9 @@ def __init__( PipelineRegister() ] * self.num_stages - # if != None: [stage to cause stall, remaining duration] + # if != None: [index of stage to cause stall, remaining duration of stall] self.stalled: list[int] | None = None - # holds the old content of the first stage, since the first stage must not be rerun + # holds the old contents of pipeline registers that are used as input for stalled stages self.stalled_pipeline_regs: list[PipelineRegister] | None = None def step(self): @@ -73,7 +73,7 @@ def step(self): continue elif ( index <= self.stalled[0] - ): # Other stalled stages should get the old PipelineRegister values NOTE: This is fine, because the only instruction that stalls in EX is ecall and ecall does not get it´s register values from the previous PipelineRegister + ): # other stalled stages should get the old PipelineRegister values stored in stalled_pipeline_regs tmp = self.pipeline_registers[index - 1] self.pipeline_registers[index - 1] = self.stalled_pipeline_regs[ index - 1 @@ -85,11 +85,13 @@ def step(self): ) self.pipeline_registers[index - 1] = tmp continue + next_pipeline_registers[index] = self.stages[index].behavior( pipeline_registers=self.pipeline_registers, index_of_own_input_register=(index - 1), state=self.state, ) + except Exception as e: if index - 1 >= 0: raise InstructionExecutionException( @@ -104,7 +106,7 @@ def step(self): else: raise - # Check if a stage has produced a stall signal + # Check if a stage has produced a meaningfull stall signal for index, pipeline_register in reversed( list(enumerate(next_pipeline_registers)) ): @@ -117,7 +119,7 @@ def step(self): self.state.performance_metrics.stalls += 1 break - # Keep the old value of the first pipeline register + # keep PipelineRegister values in stalled_pipeline_regs if self.stalled and self.stalled_pipeline_regs is None: self.stalled_pipeline_regs = self.pipeline_registers[: self.stalled[0]] diff --git a/tests/test_riscv_pipeline.py b/tests/test_riscv_pipeline.py index 4a922067..881a5b18 100644 --- a/tests/test_riscv_pipeline.py +++ b/tests/test_riscv_pipeline.py @@ -1396,3 +1396,25 @@ def test_stall(self): self.assertEqual(sim.state.performance_metrics.cycles, 8) self.assertEqual(sim.state.performance_metrics.stalls, 1) self.assertEqual(sim.state.performance_metrics.flushes, 0) + + def test_stall_2(self): + program = """ + add x0, x0, x0 + add x0, x0, x0 + addi x1, x0, 11 + addi x2, x1, 3 + addi x3, x1, 4 + addi x4, x2, 100 + """ + + sim = RiscvSimulation(mode="five_stage_pipeline") + + sim.load_program(program) + sim.run() + self.assertEqual(sim.state.register_file.registers[1], 11) + self.assertEqual(sim.state.register_file.registers[2], 14) + self.assertEqual(sim.state.register_file.registers[3], 15) + self.assertEqual(sim.state.register_file.registers[4], 114) + self.assertEqual(sim.state.performance_metrics.cycles, 14) + self.assertEqual(sim.state.performance_metrics.stalls, 2) + self.assertEqual(sim.state.performance_metrics.flushes, 0) From e0a92adf2446c3b66a6b67a0c4431481d0acff78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20M=C3=BCller?= Date: Fri, 12 Apr 2024 10:40:07 +0200 Subject: [PATCH 08/19] Exit codes now get printed again --- webgui/src/components/riscv/RiscvRegistersOutput.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/webgui/src/components/riscv/RiscvRegistersOutput.vue b/webgui/src/components/riscv/RiscvRegistersOutput.vue index 0d4b6cbe..3bbf67e8 100644 --- a/webgui/src/components/riscv/RiscvRegistersOutput.vue +++ b/webgui/src/components/riscv/RiscvRegistersOutput.vue @@ -17,6 +17,7 @@ const additionalMessageGetter = () => simulationStore.output; class="output" :simulation-store="simulationStore" :additional-message-getter + :exit-code="simulationStore.exitCode" /> From 34018a3512699cea39b40b53b9f7df744336e246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20M=C3=BCller?= Date: Mon, 15 Apr 2024 08:37:49 +0200 Subject: [PATCH 09/19] Added the performance metrics tooltip to Toy --- webgui/src/components/toy/ToyControlBar.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/webgui/src/components/toy/ToyControlBar.vue b/webgui/src/components/toy/ToyControlBar.vue index 5ccaec10..f9d9eeee 100644 --- a/webgui/src/components/toy/ToyControlBar.vue +++ b/webgui/src/components/toy/ToyControlBar.vue @@ -3,6 +3,10 @@ import ToyElementToggle from "./ToyElementToggle.vue"; import ToyControlButtons from "./ToyControlButtons.vue"; import ToyVisToggle from "./ToyVisToggle.vue"; +import PerformanceMetrics from "../PerformanceMetrics.vue"; +import { useToySimulationStore } from "@/js/toy_simulation_store"; + +const simulationStore = useToySimulationStore();