contributed by ???
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
Sign bit: 1 bit
Exponent: 8 bits
Mantissa: 23 bits
Sign bit: 1 bit
Exponent: 8 bits
Mantissa: 7 bits
This is my code
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;
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;
}
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
We test our code using Ripes simulator.
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.
The "5-stage" means this processor using five-stage pipeline to parallelize instructions. The stages are:
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.
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.
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.
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).
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).