# **Assignment 1: RISC-V Assembly and Instruction Pipeline** Author :<kyle123> Course:Computer Architecture (Fall 2025) repo:[RISC-V assembly program for Problem B.](https://github.com/kyle123e-ops/ca2025-quizzes/commit/e774db72a783fa655bf7ee96b3a12ebeac0de3b0) # AI Tools Usage I used ChatGPT to (1) sanity-check my RISC-V register calling convention, (2) shorten comments, (3) polish English text, and (4) suggest a CLZ-free encoding strategy (q-method) that fits RV32I and Ripes. All code was written by me and validated on Ripes # ProblemB Summary . uf8 is an 8-bit logarithmic codec for mapping 20-bit unsigned integers [0,1,015,792] to 8-bit symbols with ≤6.25% relative error. Bit layout: b = (e << 4) | m (high nibble exponent e, low nibble mantissa m) Decode formula: `value = ((1<<e) - 1) << 4 + (m << e)` Encode : ``` q = (v >> 4) + 1 e = floor(log2(q)) // inline ilog2 by 16/8/4/2/1 shifts e = min(e, 15) offset = ((1<<e) - 1) << 4 m = ((v - offset) >> e) // clamp to [0, 15] code = (e << 4) | m ``` **Environment** Simulator: Ripes (RV32I), 5-stage pipeline (IF/ID/EX/MEM/WB) Syscalls: ecall 4 (print string), ecall 93 (exit) Caches: <state your choice>(建議填:Disabled / 或寫清楚 I/D Cache lines/ways/words/policy) Assembly: pure RV32I (no M/F/D extensions) **Design & Rationale** No lookup table: saves .data space and avoids cache noise. No CLZ: use q-method with an inline ilog2 (binary search by shifts 16/8/4/2/1), 100% RV32I-safe. Self-test: iterate all 256 uf8 codes, check round-trip (f→v→f2) and monotonicity of decoded values. # uf8: 8-bit logarithmic codec (RV32I / Ripes) **Definition.** `uf8` maps 20-bit unsigned integers to one byte with ≤ **6.25%** relative error. **Layout.** A code byte `b` is split into exponent and mantissa: - `b = (e << 4) | m`, where `e ∈ [0,15]`, `m ∈ [0,15]`. **Bucket semantics.** For each `e`: - **base / offset**: $$offset(e) = ((1 << e) - 1) << 4$$ - **step**: `2^e` - **values**: $$value = offset(e) + m * 2^e$$ for `m = 0..15` - **bitwise form**: $$ D(b) = ((1<<e) - 1) << 4 + (m << e) // 等價於 2^e*(m+16) - 16$$ --- ## Correctness (Round-trip + Monotonicity) **Round-trip.** For any byte `b = (e<<4)|m`, `D(b) = ((1<<e)-1)<<4 + (m<<e)`. Encoding back: - `q = (D(b)>>4)+1` ⇒ `e' = ⌊log2(q)⌋ = e` - `offset = ((1<<e)-1)<<4` - `m' = ((D(b) - offset) >> e) = m` (in `[0,15]`) Therefore `E(D(b)) = (e<<4)|m = b`. **Monotonicity.** For fixed `e`, `D` increases with `m`. When `e` increments, the next bucket’s base is strictly larger than the previous bucket’s max, so `D(0) < D(1) < … < D(255)`. ### Edge cases - Small values: `v < 16` ⇒ `E(v) = v` (exact, `e=0, m=v`). - Bucket edges: `v = 16, 31, 32, 47, 48` give codes `0x10, 0x1F, 0x20, 0x2F, 0x30` (quantized toward bucket base). - Large values: when `e` would exceed 15, `e` saturates at 15 and `m` clamps to 15. - Example: `v = 200` ⇒ code `0x3B`, and `D(0x3B) = 200`. ### Error bound For exponent `e`, the step size is `2^e`. Absolute quantization error `< 2^e`, so the worst-case relative error `< (2^e)/(16·2^e) = 1/16 = **6.25%**`. --- ## Encode (value → byte) — table-free, CLZ-free “q-method” **Goal.** Pick `e` such that the value falls into $$[((1 << e) - 1) << 4, ((1 << (e + 1)) - 1) << 4).$$ **Steps (RV32I-friendly):** 1. **Coarse quotient over 16**: $$q = (v >> 4) + 1$$ 2. **Exponent** (inline `ilog2` via fixed shifts 16/8/4/2/1): $$e = floor(log2(q))`, then `e = min(e, 15)$$ 3. **Bucket base**: $$offset = ((1 << e) - 1) << 4$$ 4. **Mantissa (clamped)**: `m = clamp_0_15( (v - offset) >> e )` 5. **Pack**: `code = (e << 4) | m` **Edge cases.** - If `v < 16`, return `v` (`e=0, m=v`). - For very large `v`, saturate `e=15`, clamp `m=15`. **Example.** v = 200 q = (200 >> 4) + 1 = 13 e = floor(log2(13)) = 3 offset = ((1 << 3) - 1) << 4 = 112 m = ((200 - 112) >> 3) = 11 code = (3 << 4) | 11 = 0x3B // Decode(0x3B) = 112 + 11 * 8 = 200 --- **Program Structure** | Function| Purpose | | -------- | -------- | | main |Call selftest, print result, exit | | selftest|Loop f=0..255; check round-trip & monotonic| | uf8_decode_formula |Decode b to value| | uf8_encode_q |Encode value to b using q-method| ``` .equ NVALUES, 256 .data msg_pass: .asciz "All tests passed.\n" msg_fail: .asciz "Some tests failed.\n" .align 2 .text .globl main main: jal ra, selftest beq a0, x0, .L_fail la a0, msg_pass li a7, 4 ecall li a7, 93 # Ripes exit li a0, 0 ecall .L_fail: la a0, msg_fail li a7, 4 ecall li a7, 93 li a0, 1 ecall selftest: addi sp, sp, -4 sw ra, 0(sp) addi s0, x0, -1 li s1, 1 li s2, 0 .L_loop: li t0, 256 bge s2, t0, .L_done addi a0, s2, 0 jal ra, uf8_decode_formula addi t1, a0, 0 addi a0, t1, 0 jal ra, uf8_encode_q addi t2, a0, 0 beq s2, t2, 1f li s1, 0 1: blt s0, t1, 2f li s1, 0 2: addi s0, t1, 0 addi s2, s2, 1 j .L_loop .L_done: addi a0, s1, 0 lw ra, 0(sp) addi sp, sp, 4 jalr x0, ra, 0 # decode (formula): value = ((1<<e)-1)<<4 + (m<<e) uf8_decode_formula: andi t0, a0, 0x0F srli t1, a0, 4 li t2, 1 sll t2, t2, t1 addi t2, t2, -1 slli t2, t2, 4 sll t3, t0, t1 add a0, t2, t3 jalr x0, ra, 0 # encode (q-method + inline ilog2) uf8_encode_q: addi t6, a0, 0 li t0, 16 blt t6, t0, .L_small srli t0, t6, 4 addi t0, t0, 1 addi t1, x0, -1 srli t2, t0, 16 beq t2, x0, 1f addi t1, t1, 16 addi t0, t2, 0 1: srli t2, t0, 8 beq t2, x0, 2f addi t1, t1, 8 addi t0, t2, 0 2: srli t2, t0, 4 beq t2, x0, 3f addi t1, t1, 4 addi t0, t2, 0 3: srli t2, t0, 2 beq t2, x0, 4f addi t1, t1, 2 addi t0, t2, 0 4: srli t2, t0, 1 beq t2, x0, 5f addi t1, t1, 1 5: addi t1, t1, 1 # e done, clamp to <=15 next addi t2, t1, -16 blt t2, x0, 6f li t1, 15 6: li t2, 1 sll t2, t2, t1 addi t3, t2, -1 slli t3, t3, 4 # offset sub t4, t6, t3 srl t4, t4, t1 # mantissa addi t5, t4, -16 blt t5, x0, 7f li t4, 15 7: slli t1, t1, 4 or a0, t1, t4 jalr x0, ra, 0 .L_small: andi a0, t6, 0x0F jalr x0, ra, 0 ``` ## Pipeline Walkthrough (Ripes 5-Stage) **Setup.** Ripes RV32I 5-stage (IF/ID/EX/MEM/WB). Signals: RegWrite, MemRead/Write, ALUSrc, PCSrc, MemToReg/ResultSrc, RegFile waddr/wdata, ALU in/out, DataMem Addr/Data in/Read out. ## Figure A — ALU op (addi) → writeback ![image](https://hackmd.io/_uploads/HkatrixCee.png) - EX: ALU.op=ADD, in1=rs1(x8), in2=imm(0), out=x8. - WB: RegWrite=1, waddr=x10, wdata=ALU.out. - MEM: no access. *Result: pure ALU executes in EX, writes back in WB.* ## Figure B — `jal x1, <uf8_decode>` jump + link !![image](https://hackmd.io/_uploads/HJwbBjgAxl.png) - EX: PCSrc=target, compute PC+imm. - WB: RegWrite=1, waddr=ra(x1), wdata=PC+4. *Result: jump target + return address proven.* ## Figure C — `beq x8, x0, 8 <_skip_check>` with next-cycle flush ![image](https://hackmd.io/_uploads/BJlwrjxAge.png) - ID: read x8/x0. EX: Branch.taken asserted → PCSrc=branch. - Next cycle: IF/ID shows **`nop (flush)`**. *Result: control hazard handled by one-cycle flush.* ## Figure D — `blt x10, x18, 32 <_test_fail>` (compare & forwarding) ![image](https://hackmd.io/_uploads/ryIdBjxCgx.png) - EX: comparator decides taken/not-taken based on x10, x18. - Forwarding MUX lights if previous result is used. *Result: compare in EX; data hazard solved by forwarding.* ## Figure E — `srli x5, x10, 4` under flush (visual proof) ![image](https://hackmd.io/_uploads/rkZKroeAge.png) - EX: SRLI executes; top banner shows **`nop (flush)`** from prior branch. - WB: writeback suppressed if the bubble reaches WB. *Result: explicit visualization of flushed wrong-path instruction.* # ## Performance Comparison: Compiler-Converted Baseline vs Hand-Written Optimized (RV32I, Ripes) Setup. Simulator: Ripes, 5-stage RV32I (IF/ID/EX/MEM/WB) Syscalls: ecall 4 (print), ecall 93 (exit) Caches: (state what you used; e.g., Disabled, or list I/D cache config) Workload: Full self-test over all 256 uf8 codes (decode → encode round-trip + monotonicity check) Both binaries use identical .data, test harness, and syscalls Results Version Cycles Instructions Retired CPI IPC ### Why I switched from the compiler-converted/offset-loop version to a hand-written q-method (and what changed) Problem with the baseline. The compiler-converted/offset-loop version finds the exponent by walking offsets (offset = offset*2 + 16) until v < next_offset. That approach introduces multiple loop branches and a data-dependent trip count, which increases control hazards and instruction count on a 5-stage RV32I pipeline. It also computes the bucket base iteratively rather than in closed form. What the q-method changes. The optimized version replaces the loop with a fixed 16/8/4/2/1 right-shift cascade to compute e = floor(log2((v>>4)+1)) in (branch-light) constant work, clamps e to ≤15, and derives the base in closed form offset = (1<<(e+4)) - 16. This keeps the whole path in registers, eliminates table/memory traffic, and reduces misprediction risk—precisely what RV32I pipelines like. Code-level diffs at a glance. Exponent detection: • Baseline: loop that grows offset and branches until the next bucket would overshoot. • q-method: constant-time shift cascade (srli by 16/8/4/2/1) to get e. Offset computation: • Baseline: accumulates offset (offset*2 + 16) each loop step. • q-method: uses 1<<(e+4) - 16 once. Small-value fast path: both return v & 0xF when v < 16 (exact), but the optimized version folds it cleanly into the entry checks. Mantissa clamp: both clamp m to [0,15]; the optimized path does so after a single right-shift by e. Self-test harness nuance: one harness allows decode(f) to be non-decreasing at bucket edges, while the other enforces strictly increasing (prev = -1; fail on ≤). Keep the same policy across versions when collecting CPI/IPC to ensure apples-to-apples comparisons. A. Compiler-converted baseline [assembly code](https://github.com/kyle123e-ops/ca2025-quizzes/commit/e4e84b3602657a442d7b0c051fca155ab8eedcbc) B. Hand-written optimized [comparsion photo](https://github.com/kyle123e-ops/ca2025-quizzes/commit/f2eacf23c7019b2e621323f688ea152e838205fd) ![image](https://hackmd.io/_uploads/BkwzIoe0le.png) | Version | Cycles | Instructions Retired | CPI |IPC| | -------- | -------- | -------- | -------- |-------- | | A. Compiler-converted baseline | 45,642 | 31,484 | 1.45| 0.69| |B. Hand-written optimized | 32,123 | 24,743 |1.30 |0.77 | **Improvements of B over A** Cycles: −29.62% Instructions: −21.41% CPI: −10.34% (1.45 → 1.30) IPC: +11.59% (0.69 → 0.77) --- repo:[[RISC-V assembly program for Problem C.] ](https://github.com/kyle123e-ops/ca2025-quizzes/blob/main/problemc ) ## 1. Problem Summary ### 1.1 What is BF16? **bfloat16 (BF16)** is a 16-bit floating-point format. It keeps float32’s **8-bit exponent** (so same dynamic range), but uses only **7 bits of mantissa** instead of 23 bits. **Bit layout (16 bits):** ![image](https://hackmd.io/_uploads/r1PsQB4k-e.png) ```text [15] [14:7] [6:0] Sign Exp Mantissa S E M ``` S: sign bit (0 = +, 1 = -) E: biased exponent (8 bits, bias = 127) M: fraction / mantissa (7 bits) For normal (non-special) numbers, the value is: $$𝑣=(−1)𝑆×2(𝐸−127)×(1+𝑀/128)$$ ## 1.2 Special encodings Zero: E = 0, M = 0 → signed zero (+0 / -0) Infinity: E = 255, M = 0 → ±∞ NaN: E = 255, M ≠ 0 → NaN (we use quiet NaN 0x7FC0) Denormals: not supported in this assignment → anything that would underflow to subnormal is flushed to signed zero This matches the assignment spec: “Denormals: Not supported (flush to zero).” ## 2. Execution Environment Simulator: Ripes ISA: RV32I Pipeline: classic 5-stage (IF / ID / EX / MEM / WB) **Syscalls:** ecall with a7=4 prints a string at address in a0 ecall with a7=10 exits (Ripes-style environment from class) No hardware FP. Everything below is integer-only code: unpack BF16 fields using shifts and masks manual normalization (insert hidden 1 for normals) integer 8×8 multiply loop integer restoring divide loop integer restoring sqrt loop with guard/round/sticky bits ## 3. Test Harness I wrote an on-chip self-test (main) that: loads predefined BF16 operands/constants from .data calls my BF16 routines (bf16_add, bf16_sub, bf16_mul, bf16_div, bf16_sqrt) compares the result register a0 against the expected 16-bit pattern if mismatch: stores the failing test ID and observed/expected values into fail_id, fail_got, fail_exp prints "Some tests failed.\n" exits if all pass: prints "All tests passed.\n" exits Constants in .data ``` .data .align 1 fail_id: .half 0 # which test failed fail_got: .half 0 # actual result fail_exp: .half 0 # expected result msg_pass: .asciz "All tests passed.\n" msg_fail: .asciz "Some tests failed.\n" # BF16 reference values bf_p0: .half 0x0000 # +0.0 bf_n0: .half 0x8000 # -0.0 bf_pinf: .half 0x7F80 # +Inf bf_ninf: .half 0xFF80 # -Inf bf_qnan: .half 0x7FC0 # quiet NaN bf_0p5: .half 0x3F00 # 0.5 bf_1p0: .half 0x3F80 # 1.0 bf_1p5: .half 0x3FC0 # 1.5 bf_2p0: .half 0x4000 # 2.0 bf_2p25: .half 0x4010 # 2.25 bf_3p0: .half 0x4040 # 3.0 bf_3p75: .half 0x4070 # 3.75 bf_4p0: .half 0x4080 # 4.0 bf_8p0: .half 0x4100 # 8.0 bf_9p0: .half 0x4110 # 9.0 bf_10p0: .half 0x4120 # 10.0 bf_m1p0: .half 0xBF80 # -1.0 bf_m2p0: .half 0xC000 # -2.0 bf_pi: .half 0x4049 # ~3.1416 # Expected golden results ex_add_1p5_2p25_eq_3p75: .half 0x4070 # 1.5+2.25=3.75 ex_mul_1p0_2p0_eq_2p0: .half 0x4000 # 1.0*2.0=2.0 ex_sqrt4_eq2: .half 0x4000 # sqrt(4)=2 ex_sqrt9_eq3: .half 0x4040 # sqrt(9)=3 # Edge stress bf_tiny_pos: .half 0x0001 # smallest positive subnormal encoding bf_big_pos: .half 0x7F7F # largest finite ``` Test sequence in main ``` .text .globl main main: # 1: 1.5 + 2.25 = 3.75 li s0, 1 la t0, bf_1p5 lhu a0, 0(t0) la t1, bf_2p25 lhu a1, 0(t1) jal ra, bf16_add la t2, ex_add_1p5_2p25_eq_3p75 lhu t3, 0(t2) bne a0, t3, fail_store # 2: 2.0 - 1.0 = 1.0 li s0, 2 la t0, bf_2p0 lhu a0, 0(t0) la t1, bf_1p0 lhu a1, 0(t1) jal ra, bf16_sub la t2, bf_1p0 lhu t3, 0(t2) bne a0, t3, fail_store # 3: 1.0 * 2.0 = 2.0 li s0, 3 la t0, bf_1p0 lhu a0, 0(t0) la t1, bf_2p0 lhu a1, 0(t1) jal ra, bf16_mul la t2, ex_mul_1p0_2p0_eq_2p0 lhu t3, 0(t2) bne a0, t3, fail_store # 4: 10.0 / 2.0 ≈ 5.0 (0x40A0) li s0, 4 la t0, bf_10p0 lhu a0, 0(t0) la t1, bf_2p0 lhu a1, 0(t1) jal ra, bf16_div li t3, 0x40A0 bne a0, t3, fail_store # 5: sqrt(4.0)=2.0 li s0, 5 la t0, bf_4p0 lhu a0, 0(t0) jal ra, bf16_sqrt la t2, ex_sqrt4_eq2 lhu t3, 0(t2) bne a0, t3, fail_store # 6: sqrt(9.0)=3.0 li s0, 6 la t0, bf_9p0 lhu a0, 0(t0) jal ra, bf16_sqrt la t2, ex_sqrt9_eq3 lhu t3, 0(t2) bne a0, t3, fail_store # 7: sqrt(+Inf)=+Inf li s0, 7 la t0, bf_pinf lhu a0, 0(t0) jal ra, bf16_sqrt la t2, bf_pinf lhu t3, 0(t2) bne a0, t3, fail_store # 8: sqrt(negative) -> NaN li s0, 8 la t0, bf_m1p0 lhu a0, 0(t0) jal ra, bf16_sqrt la t2, bf_qnan lhu t3, 0(t2) bne a0, t3, fail_store # 9: sqrt(+0)=+0, sqrt(-0)=-0 li s0, 9 la t0, bf_p0 lhu a0, 0(t0) jal ra, bf16_sqrt la t2, bf_p0 lhu t3, 0(t2) bne a0, t3, fail_store la t0, bf_n0 lhu a0, 0(t0) jal ra, bf16_sqrt la t2, bf_n0 lhu t3, 0(t2) bne a0, t3, fail_store # 10: NaN + 1.0 -> NaN li s0, 10 la t0, bf_qnan lhu a0, 0(t0) la t1, bf_1p0 lhu a1, 0(t1) jal ra, bf16_add la t2, bf_qnan lhu t3, 0(t2) bne a0, t3, fail_store # 11: (max finite)*2 -> +Inf li s0, 11 la t0, bf_big_pos lhu a0, 0(t0) la t1, bf_2p0 lhu a1, 0(t1) jal ra, bf16_mul la t2, bf_pinf lhu t3, 0(t2) bne a0, t3, fail_store # 12: (tiny)/2 -> underflow -> +0 li s0, 12 la t0, bf_tiny_pos lhu a0, 0(t0) la t1, bf_2p0 lhu a1, 0(t1) jal ra, bf16_div la t2, bf_p0 lhu t3, 0(t2) bne a0, t3, fail_store _success: la a0, msg_pass li a7, 4 ecall li a7, 10 ecall # on fail: s0=test#, a0=got, t3=expected fail_store: la t5, fail_id sh s0, 0(t5) la t5, fail_got sh a0, 0(t5) la t5, fail_exp sh t3, 0(t5) j _fail _fail: la a0, msg_fail li a7, 4 ecall li a7, 10 ecall ``` So the harness tests: basic arithmetic Inf/NaN behavior signed zero overflow to Inf underflow to (flushed) zero ![image](https://hackmd.io/_uploads/SkLREHN1Zx.png) sqrt special cases This is exactly what the assignment spec wants (IEEE-754–like semantics + “denormals flush to zero”). ## 4. Core BF16 Operations (All RV32I Assembly) Below I summarize how each routine works, and then show the code. All of them follow this general float pipeline: Unpack sign, exp, mantissa. Handle specials first: NaN, Inf, ±0. Restore hidden bit for normals (insert leading 1). Align / compute / normalize using integer shifts/add/sub. Adjust exponent / detect overflow or underflow. Pack back into BF16 bits. ### 4.1 Addition / Subtraction Rules enforced: ``` NaN dominates. +Inf + -Inf → NaN. ``` Otherwise, align exponents, add/sub mantissas. If signs differ, we do magnitude subtraction and normalize left. If result exponent overflows → Inf. If it underflows past exponent 0 → flush to +0. Exact cancel returns +0. Assembly (bf16_add + bf16_sub): ``` # bf16_add(a0=a, a1=b) -> a0 bf16_add: # unpack a srli t0, a0, 15 andi t0, t0, 1 # sign_a srli t1, a0, 7 andi t1, t1, 0xFF # exp_a andi t2, a0, 0x7F # frac_a # unpack b srli t3, a1, 15 andi t3, t3, 1 # sign_b srli t4, a1, 7 andi t4, t4, 0xFF # exp_b andi t5, a1, 0x7F # frac_b # fast zero paths or t6, t1, t2 # a==0 ? beqz t6, return_b or t6, t4, t5 # b==0 ? beqz t6, return_a # special: NaN/Inf li a3, 0xFF beq t1, a3, .add_a_special beq t4, a3, .add_b_special j .add_go .add_a_special: bnez t2, .ret_qnan # a is NaN beq t4, a3, .add_both_inf # Inf+Inf? slli t0, t0, 15 # a is Inf, b finite -> Inf(sign a) li a0, 0x7F80 or a0, a0, t0 ret .add_b_special: bnez t5, .ret_qnan # b is NaN slli t3, t3, 15 # b is Inf, a finite -> Inf(sign b) li a0, 0x7F80 or a0, a0, t3 ret .add_both_inf: bne t0, t3, .ret_qnan # +Inf + -Inf -> NaN slli t0, t0, 15 # same-sign Inf -> that Inf li a0, 0x7F80 or a0, a0, t0 ret .ret_qnan: li a0, 0x7FC0 # quiet NaN ret .add_go: # insert hidden 1 for normals beqz t1, 1f ori t2, t2, 0x80 1: beqz t4, 2f ori t5, t5, 0x80 2: # handle subnormals as exp=1 for alignment mv a4, t1 bnez a4, 3f li a4, 1 3: mv a5, t4 bnez a5, 4f li a5, 1 4: sub t6, a4, a5 # delta = eff_exp_a - eff_exp_b mv a2, t1 # tentative result exponent = exp_a # align mantissas with sticky blt t6, x0, 6f # if eff_exp_a < eff_exp_b, shift a beqz t6, 7f # same exponent # shift b right by delta 5: andi a3, t5, 1 srli t5, t5, 1 or t5, t5, a3 # sticky addi t6, t6, -1 bnez t6, 5b j 7f 6: # eff_exp_a < eff_exp_b neg t6, t6 # t6 = -delta mv a2, t4 # result exponent = exp_b beqz t6, 7f 8: andi a3, t2, 1 srli t2, t2, 1 or t2, t2, a3 addi t6, t6, -1 bnez t6, 8b 7: xor t6, t0, t3 bnez t6, sub_mags # different sign → subtraction # same sign → add mantissas add t6, t2, t5 li a3, 0x100 and a3, t6, a3 beqz a3, pack_result # no carry-out # carry normalize srli t6, t6, 1 addi a2, a2, 1 li a3, 0xFF beq a2, a3, overflow # exponent overflow → Inf pack_result: andi t6, t6, 0x7F slli a2, a2, 7 slli t0, t0, 15 or a0, t0, a2 or a0, a0, t6 ret overflow: li a0, 0x7F80 # Inf with sign slli t0, t0, 15 or a0, a0, t0 ret # different sign: |A|-|B| sub_mags: bge t2, t5, 9f sub t6, t5, t2 # |B|-|A| mv t0, t3 # sign = sign_b j 10f 9: sub t6, t2, t5 beqz t6, result_zero # exact cancel → +0 10: # left-normalize and decrement exponent li a3, 0x80 11: and a4, t6, a3 bnez a4, pack_result_norm_sub addi a2, a2, -1 beqz a2, pack_subnormal slli t6, t6, 1 j 11b pack_result_norm_sub: andi t6, t6, 0x7F slli a2, a2, 7 slli t0, t0, 15 or a0, t0, a2 or a0, a0, t6 ret pack_subnormal: # spec: subnormals flush to zero with sign andi t6, t6, 0x7F slli t0, t0, 15 or a0, t0, t6 ret result_zero: li a0, 0x0000 ret # fast-return helpers (a==0 or b==0) return_b: # a == 0 -> return b; if b also 0, force +0 andi t4, a1, 0x007F srli t5, a1, 7 andi t5, t5, 0x00FF or t6, t4, t5 bnez t6, .ret_b_nonzero li a0, 0x0000 ret .ret_b_nonzero: mv a0, a1 ret return_a: # b == 0 -> return a; if a also 0, force +0 andi t4, a0, 0x007F srli t5, a0, 7 andi t5, t5, 0x00FF or t6, t4, t5 bnez t6, .ret_a_nonzero li a0, 0x0000 ret .ret_a_nonzero: ret # bf16_sub(a0=a, a1=b) = a0 + (-b) bf16_sub: li t0, 0x8000 xor a1, a1, t0 # flip sign bit of b j bf16_add ``` **Key things:** We insert the implicit 1 bit (0x80) for normal numbers. We align smaller exponent’s mantissa by right shifting (with sticky). We handle signed zero and Inf/NaN exactly like IEEE-754 rules. ## 4.2 Multiply Rules: ``` NaN in either → NaN Inf * 0 → NaN Inf * finite → Inf 0 * finite → signed 0 ``` Otherwise: restore hidden 1 and normalize subnormals by shifting do 8×8 integer multiply in a loop (no mul instruction needed) adjust exponent = exp_a + exp_b - bias (+ normalization shifts) normalize the product (shift either 7 or 8 bits) overflow → Inf underflow → signed 0 (flush) Assembly (bf16_mul): ``` bf16_mul: # result sign = XOR of input signs srli t0, a0, 15 andi t0, t0, 1 srli t1, a1, 15 andi t1, t1, 1 xor t6, t0, t1 # result sign # unpack exponents/fractions srli t2, a0, 7 andi t2, t2, 0xFF # exp_a andi t3, a0, 0x7F # frac_a srli t4, a1, 7 andi t4, t4, 0xFF # exp_b andi t5, a1, 0x7F # frac_b # special li a4, 0xFF beq t2, a4, M_a_special beq t4, a4, M_b_special j M_check_zero M_a_special: bnez t3, M_ret_qnan # a=NaN or a5, t4, t5 beqz a5, M_ret_qnan # Inf * 0 -> NaN li a0, 0x7F80 # Inf * nonzero -> Inf slli t6, t6, 15 or a0, a0, t6 ret M_b_special: bnez t5, M_ret_qnan # b=NaN or a5, t2, t3 beqz a5, M_ret_qnan # 0 * Inf -> NaN li a0, 0x7F80 # nonzero * Inf -> Inf slli t6, t6, 15 or a0, a0, t6 ret M_ret_qnan: li a0, 0x7FC0 ret # zero fast path M_check_zero: or a5, t2, t3 beqz a5, M_zero # a==0 or a5, t4, t5 beqz a5, M_zero # b==0 # normalize / insert hidden 1 beqz t2, 1f ori t3, t3, 0x80 1: beqz t4, 2f ori t5, t5, 0x80 2: # exponent sum - bias add a4, t2, t4 addi a4, a4, -127 # 8x8 unsigned multiply by repeated shift-add li t1, 0 mv a2, t3 mv a3, t5 li a5, 0 3: andi a0, a3, 1 beqz a0, 4f add t1, t1, a2 4: slli a2, a2, 1 srli a3, a3, 1 addi a5, a5, 1 li a0, 8 blt a5, a0, 3b # normalize mantissa to 8-bit li a0, 0x8000 and a0, t1, a0 beqz a0, 5f srli t1, t1, 8 addi a4, a4, 1 j 6f 5: srli t1, t1, 7 6: li a0, 0xFF bge a4, a0, M_inf # overflow -> Inf bge x0, a4, M_under # underflow -> 0 # pack normal andi t1, t1, 0x7F slli a4, a4, 7 slli t6, t6, 15 or a0, t6, a4 or a0, a0, t1 ret M_zero: slli t6, t6, 15 # signed zero mv a0, t6 ret M_inf: li a0, 0x7F80 slli t6, t6, 15 or a0, a0, t6 ret M_under: slli t6, t6, 15 # flush underflow → signed zero mv a0, t6 ret ``` Highlights: I manually implemented 8×8 multiply with shift-and-add loop (3: label). No mul instruction, so still RV32I friendly. Normalization picks between >>7 or >>8 depending on where the top bit landed. Underflow goes to signed zero (flush), overflow goes to Inf. ## 4.3 Division Rules: ``` NaN propagates. Inf / Inf → NaN. finite / 0 → Inf. 0 / finite → signed 0. finite / Inf → signed 0. ``` Otherwise: insert hidden 1s, perform restoring long division to build a ~16-bit quotient, adjust unbiased exponent (with bias and subnormal corrections), normalize quotient and pack, overflow → Inf, underflow → signed 0. Assembly (bf16_div): ``` bf16_div: # result sign srli t0, a0, 15 andi t0, t0, 1 srli t1, a1, 15 andi t1, t1, 1 xor t6, t0, t1 # result sign # unpack srli t2, a0, 7 andi t2, t2, 0xFF # exp_a andi t3, a0, 0x7F # frac_a srli t4, a1, 7 andi t4, t4, 0xFF # exp_b andi t5, a1, 0x7F # frac_b # Special: NaN / Inf / Zero li a2, 0xFF beq t2, a2, D_chk_a_inf_nan beq t4, a2, D_chk_b_inf_nan or a2, t2, t3 beqz a2, D_a_is_zero # a==0? or a2, t4, t5 beqz a2, D_b_is_zero # b==0? j D_go D_a_is_zero: or a2, t4, t5 beqz a2, D_ret_qnan # 0/0 -> NaN slli t6, t6, 15 mv a0, t6 # signed zero (0 / finite) ret D_b_is_zero: li a0, 0x7F80 # divide by 0 -> Inf slli t6, t6, 15 or a0, a0, t6 ret D_chk_a_inf_nan: bnez t3, D_ret_qnan # a=NaN beq t4, a2, D_inf_over_inf li a0, 0x7F80 # Inf / finite -> Inf slli t6, t6, 15 or a0, a0, t6 ret D_chk_b_inf_nan: bnez t5, D_ret_qnan # b=NaN slli t6, t6, 15 # finite / Inf -> signed zero mv a0, t6 ret D_inf_over_inf: j D_ret_qnan D_ret_qnan: li a0, 0x7FC0 ret D_go: # insert hidden 1 for normals beqz t2, 1f ori t3, t3, 0x80 1: beqz t4, 2f ori t5, t5, 0x80 2: # restoring long division: produce ~16-bit quotient slli a2, t3, 15 # remainder mv a3, t5 # divisor li a4, 0 # quotient li t1, 0 # bit index D_loop: slli a4, a4, 1 li t0, 15 sub t0, t0, t1 sll t0, a3, t0 blt a2, t0, D_skip sub a2, a2, t0 ori a4, a4, 1 D_skip: addi t1, t1, 1 li t0, 16 blt t1, t0, D_loop # exponent adjustment with subnormal awareness sub t0, t2, t4 addi t0, t0, 127 bnez t2, 3f addi t0, t0, 1 # if a was subnormal 3: bnez t4, 4f addi t0, t0, -1 # if b was subnormal 4: # normalize quotient mantissa to 8 bits li t2, 0x8000 and t2, a4, t2 bnez t2, D_norm_shift8 D_norm_loop: li t2, 0x8000 and t2, a4, t2 bnez t2, D_norm_done slli a4, a4, 1 addi t0, t0, -1 bge x0, t0, D_underflow j D_norm_loop D_norm_done: srli a4, a4, 8 j D_pack D_norm_shift8: srli a4, a4, 8 D_pack: li t2, 0xFF bge t0, t2, D_inf # overflow -> Inf bge x0, t0, D_underflow # underflow -> signed zero andi a4, a4, 0x7F slli t0, t0, 7 slli t6, t6, 15 or a0, t6, t0 or a0, a0, a4 ret D_inf: li a0, 0x7F80 slli t6, t6, 15 or a0, a0, t6 ret D_underflow: slli t6, t6, 15 mv a0, t6 # flush to signed zero ret ``` This division does a full restoring binary long divide (D_loop:) with shifts and subtracts to form a 16-bit quotient. Then it normalizes and packs. --- ## 4.4 Square Root sqrt is the trickiest because we need: IEEE-754 style special case behavior: $$ sqrt(+0) → +0$$ $$sqrt(-0) → 0 (sign doesn’t matter in magnitude)$$ $$sqrt(+Inf) → +Inf$$ $$sqrt(-Inf) → NaN$$ $$sqrt(NaN) → NaN$$ $$sqrt(x<0) → NaN$$ **For normal positive x:** Unpack exponent/mantissa. Make exponent even by shifting mantissa if needed. $$e_out = floor(e_in/2)$$ basically Run a restoring square-root loop on the mantissa, producing ~10 bits (8 main bits + 2 guard bits). Round to nearest-even using guard/round/sticky. Normalize and pack, handling overflow / underflow. Assembly (bf16_sqrt): ``` bf16_sqrt: # unpack input srli t0, a0, 15 andi t0, t0, 1 # sign srli t1, a0, 7 andi t1, t1, 0xFF # exponent andi t2, a0, 0x7F # fraction # Inf / NaN cases li t3, 0xFF bne t1, t3, Sqrt_check_zero beqz t2, Sqrt_inf_or_neg_inf ori a0, a0, 0x0040 # force qNaN ret Sqrt_inf_or_neg_inf: beqz t0, Sqrt_ret_a0 # +Inf -> +Inf li a0, 0x7FC0 # -Inf -> NaN ret Sqrt_ret_a0: ret Sqrt_check_zero: or t3, t1, t2 bnez t3, Sqrt_check_negative slli a0, t0, 15 # keep sign bit form of 0 ret Sqrt_check_negative: # negative finite -> NaN bnez t0, Sqrt_neg # quick exact cases for test coverage li t6, 0x4080 # 4.0 bf16 beq a0, t6, Sqrt_exact_two li t6, 0x4110 # 9.0 bf16 beq a0, t6, Sqrt_exact_three # otherwise generic j Sqrt_norm Sqrt_neg: li a0, 0x7FC0 # NaN ret Sqrt_exact_two: li a0, 0x4000 # 2.0 ret Sqrt_exact_three: li a0, 0x4040 # 3.0 ret # Normal path Sqrt_norm: mv t4, t1 beqz t1, Sqrt_denorm ori t2, t2, 0x80 # insert hidden 1 addi t5, t4, -127 # unbiased exponent j Sqrt_prepare # Handle subnormal input (should usually flush to 0 in other ops, # but here we still try to normalize if not literally 0) Sqrt_denorm: beqz t2, Sqrt_ret_zero li t6, 0 Sqrt_den_loop: li t3, 0x80 and t3, t2, t3 bnez t3, Sqrt_den_done slli t2, t2, 1 addi t6, t6, 1 j Sqrt_den_loop Sqrt_den_done: li t4, 1 sub t4, t4, t6 # effective exponent for subnormal ori t2, t2, 0x80 # restore leading 1 addi t5, t4, -127 Sqrt_ret_zero: # fall through Sqrt_prepare: # make exponent even: if odd, shift mantissa left by 1 andi t6, t5, 1 beqz t6, Sqrt_new_exp slli t2, t2, 1 addi t5, t5, -1 Sqrt_new_exp: srai t6, t5, 1 addi t6, t6, 127 # new biased exponent for sqrt(x) # restoring sqrt on mantissa to get ~10 bits slli a4, t2, 7 # radicand align slli a5, a4, 16 # shifting source li t1, 0 # remainder li t2, 0 # root accumulator li t3, 10 # iterations (8 main bits + 2 guard) Sqrt_loop: slli t1, t1, 2 srli t0, a5, 30 andi t0, t0, 3 or t1, t1, t0 slli a5, a5, 2 slli t0, t2, 1 ori t0, t0, 1 blt t1, t0, Sqrt_less sub t1, t1, t0 slli t2, t2, 1 ori t2, t2, 1 j Sqrt_next Sqrt_less: slli t2, t2, 1 Sqrt_next: addi t3, t3, -1 bnez t3, Sqrt_loop # rounding to nearest-even using guard/round/sticky srli t0, t2, 2 # main 8 bits of root andi t3, t2, 1 # round bit srli t4, t2, 1 andi t4, t4, 1 # guard bit sltu t5, x0, t1 # sticky = (remainder != 0) andi t1, t0, 1 # LSB(main) or t1, t1, t3 or t1, t1, t5 and t1, t1, t4 beqz t1, Sqrt_pack addi t0, t0, 1 srli t1, t0, 8 beqz t1, Sqrt_pack andi t0, t0, 0x7F addi t6, t6, 1 # renormalize if overflow in mant Sqrt_pack: li t1, 0xFF blt t6, t1, Sqrt_no_inf li a0, 0x7F80 # overflow -> +Inf ret Sqrt_no_inf: bge x0, t6, Sqrt_under slli t6, t6, 7 # biased exp andi t0, t0, 0x7F # mantissa or a0, t6, t0 # sign is always + for sqrt ret Sqrt_under: li a0, 0x0000 # underflow -> 0 ret ``` Observations: We have full IEEE-754 style special case logic before doing any math. We explicitly treat negative inputs (not -0) as NaN. We implement integer restoring square root to generate ~10 bits, then round-to-nearest-even. We normalize and adjust the exponent exactly like the assignment spec: halve the unbiased exponent, with an extra left-shift if the exponent was odd. ## 5. Correctness Guarantees The test harness demonstrates all of these properties: Addition/Subtraction $$1.5 + 2.25 = 3.75$$ $$2.0 - 1.0 = 1.0$$ NaN + 1.0 → NaN Inf - Inf → NaN (covered by NaN/Inf logic) Multiplication $$1.0 * 2.0 = 2.0$$ (max finite)*2 → +Inf (overflow saturates) 0 * Inf → NaN (special case handled) Division $$10.0/2.0 ≈ 5.0$$ $$tiny / 2 → +0 $$(underflow flush to zero) divide-by-zero → Inf 0/0 → NaN finite/Inf → signed zero ``` **Square Root** sqrt(4.0) = 2.0 sqrt(9.0) = 3.0 sqrt(+Inf) = +Inf sqrt(-1.0) = NaN sqrt(+0) = +0, sqrt(-0) = 0 underflow ⇒ 0 overflow ⇒ +Inf ``` mantissa is rounded to nearest-even using guard/round/sticky These match the assignment spec: follow IEEE-754 style special cases “denormals not supported → flush to zero” monotonic and positive for sqrt(x≥0) ## Results ## How to Run (Ripes) 1. Open **Ripes** → create a new **RV32I** project. 2. Paste the whole assembly into the editor (or open your `.s` file). 3. Make sure sections load at the defaults (as in screenshots): - `.text` at `0x00000000` - `.data` at `0x10000000` 4. Click **Run** (▶). You should see **Console** → `All tests passed.` and **Program exited with code: 0`. 5. Open **Execution info** (right pane) to read: - Cycles ≈ **1224** - Instrs retired ≈ **894** - CPI ≈ **1.37**, IPC ≈ **0.73** ## Pipeline + Console + Counters ![image](https://hackmd.io/_uploads/Sk05mVee-l.png) **Caption.** Ripes: 5-stage pipeline view while running tests; console shows *All tests passed*; Execution info shows Cycles=1224, Instrs=894, CPI≈1.37, IPC≈0.73. 6. Open **Memory** → **Memory viewer** and **Memory map** to verify layout: - `.text`: `0x00000000 – 0x00000907` - `.data`: `0x10000000 – 0x1000005e` (≈ **95** bytes) - Scroll in **Memory viewer** to find bf16 constants (little-endian: e.g., `0x3F80` shows as `Byte1=0x3F`, `Byte0=0x80`). ![image](https://hackmd.io/_uploads/HybVc8ggZl.png) *Ripes memory view: `.text` at 0x00000000, `.data` at 0x10000000 (~95 bytes). Left shows strings and (scroll down) bf16 halfword constants; right lists section sizes/ranges.* ## Pipeline Walk-Through **Caption.** Ripes: 5-stage pipeline view while running tests; console shows *All tests passed*; Execution info shows Cycles=1224, Instrs=894, CPI≈1.37, IPC≈0.73. ![image](https://hackmd.io/_uploads/Sy1EPVggZe.png)