Assignment1: converting float to integer

contributed by ???

Quiz 1 Problem B

Through learning about single-precision and implementing quiz 1 b through programming, practice converting fp32 to bf16, and, extending this, use bit-shifting to round floating-point numbers and convert them to integers

Structure Comparison:

fp32 (IEEE 754)

Sign bit: 1 bit
Exponent: 8 bits
Mantissa: 23 bits

bf16 (Brain Floating-Point Format)

Sign bit: 1 bit
Exponent: 8 bits
Mantissa: 7 bits

How to Convert from fp32 to bf16:

  1. Preserve the Sign bit: The sign bit remains unchanged during the conversion from fp32 to bf16.
  2. Preserve the Exponent: Since bf16 is designed with the same size of the exponent part as fp32, we retain the exponent part of fp32 directly during the conversion.
  3. Truncate or Round the Mantissa: Because the mantissa of bf16 only has 7 bits, which is much smaller than fp32, we need to reduce the mantissa of fp32 to 7 bits. Some strategies might be needed to decide how to reduce from 23 bits to 7 bits, such as truncation, round to nearest even, or rounding towards positive or negative infinity, etc.

This is my code

Implementation

fp32tobf16.c

The main purpose of this function is to convert a 32-bit floating-point number into a 16-bit bfloat.

#include <stdio.h> float fp32_to_bf16(float x) { float y = x; int *p = (int *) &y; unsigned int exp = *p & 0x7F800000; unsigned int man = *p & 0x007FFFFF; if (exp == 0 && man == 0) /* zero */ { printf("exp==0 && man==0\n"); return x; } if (exp == 0x7F800000) /* infinity or NaN */ { printf("exp==255"); return x; } /* Normalized number */ /* round to nearest */ float r = x; int *pr = (int *) &r; *pr &= 0xFF800000; /* r has the same exp as x */ r /= 0x100; y = x + r; *p &= 0xFFFF0000; return y; }

This line of code type-casts the address of y (which is a float) to an integer pointer. In other words, the pointer p now points to the memory address of y, but p will interpret the bits at that memory location as an integer (int) rather than a floating-point number (float).

    int *p = (int *) &y;

The first two lines of code use bitwise AND operations to extract the exponent and mantissa parts of x. The masks 0x7F800000 and 0x007FFFFF are used to extract the exponent and mantissa from x, respectively. Next, the first 'if' statement checks whether x is 0. If both the exponent and mantissa are 0, the function outputs a message and returns x. The second 'if' statement checks whether x is infinite or NaN. If the exponent part equals 0x7F800000, the function outputs a message and returns x."

    unsigned int exp = *p & 0x7F800000;
    unsigned int man = *p & 0x007FFFFF;

    if (exp == 0 && man == 0) /* zero */
        {
            printf("exp==0 && man==0\n");
            return x;
        }
    if (exp == 0x7F800000) /* infinity or NaN */
        {
            printf("exp==255");
            return x;
        }

Since the bit count for the sign and exponent of bf16 is consistent, the part for the exponent's mask is the same. Also, it achieves rounding through division by 256 (0x100), and finally uses the 0xFFFF0000 mask to retain the final bf16 format.

    float r = x;
    int *pr = (int *) &r;
    *pr &= 0xFF800000;  /* r has the same exp as x */
    r /= 0x100;
    y = x + r;
    
    *p &= 0xFFFF0000;

round_function.c

This is a program that converts a floating-point number to an integer through bit-shift operations and understanding the single-precision floating-point format, aiming to deepen the understanding of floating-point numbers. I also planned to try writing a conversion suitable for the bf16 format, but since the stored format remains as a float format in the C language after conversion, I continued to use this function, and it can be used universally

#include <stdio.h> #include <stdint.h> #include <math.h> #include <limits.h> // Define the structure for IEEE 754 single-precision floating-point numbers struct IEEE754_single { uint32_t mantissa : 23; // 23 bits for the mantissa uint32_t exponent : 8; // 8 bits for the exponent uint32_t sign : 1; // 1 bit for the sign }; struct bf16_struct { uint16_t mantissa : 7; uint16_t exponent : 8; uint16_t sign : 1; }; // Function to round a floating-point number to the nearest integer int round_to_nearest_integer(float x) { // Create a union for IEEE 754 single-precision floating-point numbers union { float f; struct IEEE754_single bits; } ieee754; ieee754.f = x; // Assign the floating-point number to the union // Extract the sign, exponent, and mantissa parts int sign = ieee754.bits.sign; int exponent = ieee754.bits.exponent - 127; // Offset of the exponent is 127 uint32_t mantissa = ieee754.bits.mantissa | 0x800000; // Add the hidden 1 // Handle the case when the input value is 0 if (ieee754.bits.exponent == 0 && ieee754.bits.mantissa == 0) { return 0; } if (ieee754.bits.exponent == 255 && ieee754.bits.mantissa == 0) { if (sign) { printf("the input value is too small\n"); return INT_MIN; // Negative infinity } else { printf("the input value is too large\n"); return INT_MAX; // Positive infinity } } // Check if the input value is too large or too small if (exponent > 31) { if (sign) { printf("the input value is too small\n"); return INT_MIN; // Negative infinity } else { printf("the input value is too large\n"); return INT_MAX; // Positive infinity } } // Check if the input value is NaN if (ieee754.bits.exponent == 255 && ieee754.bits.mantissa != 0) { // NaN value detected if (mantissa & (1 << 22)) { printf("This is a signaling NaN (sNaN)\n"); // Handle sNaN (signaling NaN) as needed return 0; } else { printf("This is a quiet NaN (qNaN)\n"); // Handle qNaN (quiet NaN) as needed return 0; } } // Perform rounding int int_value; // Integer part (before the decimal point) int float_value; // Fractional part (after the decimal point) if (exponent >= 23) { int_value = mantissa << (exponent - 23); float_value = 0; // No fractional part } else { int_value = mantissa >> (23 - exponent); float_value = mantissa & ((1 << (23 - exponent)) - 1); // Get the fractional part } // Perform rounding by adding the last fractional bit to the integer part if ((float_value & (1 << (22 - exponent))) != 0) { int_value += 1; } // Determine the final sign of the result based on the sign bit if (sign) { int_value = -int_value; } return int_value; }

If the exponent is greater than or equal to 23, then the mantissa is left-shifted by (exponent−23)bits, moving the decimal point to the appropriate position to the right, and the fractional part float_value is set to 0, because at this point all of the fractional bits will be shifted out of range.
If the exponent is less than 23, the mantissa is right-shifted by (23−exponent) bits, moving the decimal point to the appropriate position to the left, and bitwise AND operations and left shift operations are used to obtain the bit representation of the fractional part."

// Perform rounding
    int int_value;   // Integer part (before the decimal point)
    int float_value; // Fractional part (after the decimal point)
    if (exponent >= 23)
    {
        int_value = mantissa << (exponent - 23);
        float_value = 0; // No fractional part
    }
    else
    {
        int_value = mantissa >> (23 - exponent);
        float_value = mantissa & ((1 << (23 - exponent)) - 1); // Get the fractional part
    }

Here, it checks whether the most significant bit of the fractional part is 1. The expression
float_value&(1<<(22−exponent))is used to check the most significant bit of the fractional part: if it is not 0, indicating the fractional part is greater than 0.5, we will round up the integer part.

// Perform rounding by adding the last fractional bit to the integer part
    if ((float_value & (1 << (22 - exponent))) != 0)
    {
        int_value += 1;
    }

Assembly code

This assembly language implements the functionality of the function 'float fp32_to_bf16(float x)' from quiz 1b.

.data pi: .word 0x40490FDB # 3.1415926 val_875: .word 0x410C0000 # 8.75 val_7523: .word 0x40F085C3 # 7.523 zeromsg: .string "zero\n" nanmsg: .string "infiniti or NaN\n" .text .globl _start _start: # Set up stack space for a single word addi sp, sp, -4 # allocate 4 bytes on the stack # Input FP32 value, store it on the stack lw a0, val_875 # load data sw a0, 0(sp) # store data to stack # Retrieve Exponent and Mantissa lw a1, 0(sp) li a2, 0x7F800000 and a3, a1, a2 # a3 = exp li a2, 0x007FFFFF and a4, a1, a2 # a4 = man # Check if it is zero beqz a3, is_zero beqz a4, is_zero # Check if it is infinity or NaN li a2, 0x7F800000 beq a3, a2, is_inf_or_nan # Perform rounding li a2, 0xFF800000 li t0, 0xFFFF0000 and a1, a1, a2 srli a1, a1, 8 # Divide by 0x100 add a1, a1, a0 and a0, a1, t0 j end is_zero: la a0, zeromsg li a7, 4 # system call for printing string ecall j end is_inf_or_nan: la a0, nanmsg li a7, 4 # system call for printing string ecall j end end: # Exit simulation li a7, 10 # system call for exit ecall

Analysis

We test our code using Ripes simulator.

Pseudo instruction

Put code above into editor and we will see that Ripe doesn't execute it literally. Instead, it replace pseudo instruction (p.110) into equivalent one, and change register name from ABI name to sequencial one.
The translated code looks like:

00000000 <_start>:
    0:        ffc10113        addi x2 x2 -4
    4:        10000517        auipc x10 0x10000
    8:        00052503        lw x10 0 x10
    c:        00a12023        sw x10 0 x2
    10:        00012583        lw x11 0 x2
    14:        7f800637        lui x12 0x7f800
    18:        00c5f6b3        and x13 x11 x12
    1c:        00800637        lui x12 0x800
    20:        fff60613        addi x12 x12 -1
    24:        00c5f733        and x14 x11 x12
    28:        02068663        beq x13 x0 44 <is_zero>
    2c:        02070463        beq x14 x0 40 <is_zero>
    30:        7f800637        lui x12 0x7f800
    34:        02c68a63        beq x13 x12 52 <is_inf_or_nan>
    38:        ff800637        lui x12 0xff800
    3c:        ffff02b7        lui x5 0xffff0
    40:        00c5f5b3        and x11 x11 x12
    44:        0085d593        srli x11 x11 8
    48:        00a585b3        add x11 x11 x10
    4c:        0055f533        and x10 x11 x5
    50:        02c0006f        jal x0 44 <end>

00000054 <is_zero>:
    54:        10000517        auipc x10 0x10000
    58:        fb850513        addi x10 x10 -72
    5c:        00400893        addi x17 x0 4
    60:        00000073        ecall
    64:        0180006f        jal x0 24 <end>

00000068 <is_inf_or_nan>:
    68:        10000517        auipc x10 0x10000
    6c:        faa50513        addi x10 x10 -86
    70:        00400893        addi x17 x0 4
    74:        00000073        ecall
    78:        0040006f        jal x0 4 <end>

0000007c <end>:
    7c:        00a00893        addi x17 x0 10
    80:        00000073        ecall

In each row it denotes address in instruction memory, instruction's machine code (in hex) and instruction itself respectively.

5-stage Risc-V processor

The "5-stage" means this processor using five-stage pipeline to parallelize instructions. The stages are:

  1. Instruction fetch (IF)
  2. Instruction decode and register fetch (ID)
  3. Execute (EX)
  4. Memory access (MEM)
  5. Register write back (WB)

WB (Write Back) Stage

Instruction: lui x12, 0xff800
In the WB stage, the processor writes the execution result back to the register. For the lui instruction (that is, "Load Upper Immediate"), this stage will left-shift the immediate value by 12 bits and store it in the destination register x12.

MEM (Memory Access) Stage

Instruction: lui x5, 0xffff0
In the MEM stage, if the instruction involves memory access (for example, load and store instructions), relevant operations will be performed. However, the lui instruction does not involve memory access, so there are no actual operations in this stage.

EX (Execute) Stage

Instruction: and x11, x11, x12
This stage is used for arithmetic and logical operations. The and instruction performs a bitwise AND operation: it executes an AND operation on the bits between x11 and x12, and then stores the result in x11.

ID (Instruction Decode) Stage

Instruction: srli x11, x11, 8
In the ID stage, the processor decodes the instruction fetched in the IF stage and reads the necessary operands. For the srli (that is, logical right shift) instruction, the processor parses the destination register (here, x11), source register (also x11 here), and the number of bits to be right-shifted (here, 8).

IF (Instruction Fetch) Stage

Instruction: add x11, x11, x10
In the IF stage, the processor retrieves the next instruction to be executed from the instruction memory. Here, it fetches the add instruction and pre-prepares to pass it to the next stage (ID stage).


Reference

Select a repo