From a66351906231b9bbf6fb4aea49b0da5f57de1972 Mon Sep 17 00:00:00 2001 From: Haimrich Date: Tue, 3 Jun 2025 11:04:57 +0200 Subject: [PATCH 1/6] [CARFIELD] fix plic init disabling fpu --- examples/targets/carfield/config/Makefile | 10 +++++----- .../carfield/libs/carfield_lib/include/carfield.h | 2 +- .../carfield/libs/carfield_lib/src/carfield.c | 13 +++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/targets/carfield/config/Makefile b/examples/targets/carfield/config/Makefile index 8cce6e5..9ea9514 100644 --- a/examples/targets/carfield/config/Makefile +++ b/examples/targets/carfield/config/Makefile @@ -47,7 +47,7 @@ export PULP_SDK_HOME = $(PULPRT_HOME) PULP_APPS = pulp_cluster_runtime PULP_APP_SRCS = $(MATCH_COMMON_SRCS) $(MATCH_pulp_cluster_OFFLOAD_SRC) $(PULPRT_HOME)/lib/libc/minimal/io.c $(PULPRT_HOME)/lib/libc/minimal/prf.c -PULP_CFLAGS = -O3 $(MATCH_INCLUDES) -DCLUSTER_COMPILATION -DARCHI_CLUSTER_NB_PE=8 -I$(PULPRT_HOME)/lib/libc/minimal/include -D__pulp_cluster__ +PULP_CFLAGS = -O3 $(MATCH_INCLUDES) -DCLUSTER_COMPILATION -DARCHI_CLUSTER_NB_PE=8 -I$(PULPRT_HOME)/lib/libc/minimal/include -D__pulp_cluster__ -Dhalf=float16 -D_Float16=float16 PULPD_ELF_REMOVE_SECTIONS := --remove-section .l1cluster_g --remove-section .bss_l1 -include $(PULP_SDK_HOME)/install/rules/pulp.mk @@ -61,7 +61,7 @@ build-offload: $(PULPD_RISCV)-objcopy $(PULPD_ELF_REMOVE_SECTIONS) $(BUILD_DIR)/build/pulp_cluster_runtime/pulp_cluster_runtime; @echo "Generating objdump..." - $(PULPD_RISCV)-objdump -d -S $(BUILD_DIR)/build/pulp_cluster_runtime/pulp_cluster_runtime > $(BUILD_DIR)/build/pulp_cluster_runtime/pulp_cluster_runtime.dump; + $(PULPD_RISCV)-objdump -drwCS $(BUILD_DIR)/build/pulp_cluster_runtime/pulp_cluster_runtime > $(BUILD_DIR)/build/pulp_cluster_runtime/pulp_cluster_runtime.dump; @echo "Runtime offload build done." @@ -74,7 +74,7 @@ build-offload: CAR_SW_DIR := $(CAR_ROOT)/sw CHS_ROOT ?= $(shell $(BENDER) path cheshire) -CHS_SW_GCC_BINROOT ?= /usr/pack/riscv-1.0-kgf/riscv64-gcc-11.2.0/bin +CHS_SW_GCC_BINROOT ?= /usr/pack/riscv-1.0-kgf/riscv64-gcc-14.2.0/bin -include $(CHS_ROOT)/cheshire.mk CHS_BOOTMODE ?= 0 # default passive bootmode @@ -105,7 +105,7 @@ $(HOST_LIB): $(HOST_LIB_SRCS_O) $(CAR_SW_DIR)/%.car.o: $(CAR_SW_DIR)/%.c $(CHS_SW_CC) $(CAR_SW_INCLUDES) $(CHS_SW_CCFLAGS) -c $< -o $@ -HOST_FLAGS := -T$(HOST_LD_SCRIPT) -Wno-pointer-to-int-cast -DIntClustNumCores=8 -g +HOST_FLAGS := -T$(HOST_LD_SCRIPT) -Wno-pointer-to-int-cast -DIntClustNumCores=8 -Dhalf=_Float16 -g -march=rv64gc_zifencei @@ -123,5 +123,5 @@ build-host: $(HOST_LIB) build-payload @echo $(HOST_LIB_SRCS_O) $(CHS_SW_CC) $(HOST_INCLUDES) $(MATCH_INCLUDES) $(CHS_SW_LDFLAGS) $(HOST_FLAGS) -o $(BUILD_DIR)/host.elf $(HOST_LIB) $(MATCH_COMMON_SRCS) $(MATCH_HOST_SRC) $(CHS_SW_LIBS) @echo "Generating objdump" - @$(CHS_SW_OBJDUMP) -d -S $(BUILD_DIR)/host.elf > $(BUILD_DIR)/host.dump + @$(CHS_SW_OBJDUMP) -drwCS $(BUILD_DIR)/host.elf > $(BUILD_DIR)/host.dump @echo "Host build done" \ No newline at end of file diff --git a/examples/targets/carfield/libs/carfield_lib/include/carfield.h b/examples/targets/carfield/libs/carfield_lib/include/carfield.h index 7698cde..4c67670 100644 --- a/examples/targets/carfield/libs/carfield_lib/include/carfield.h +++ b/examples/targets/carfield/libs/carfield_lib/include/carfield.h @@ -39,7 +39,7 @@ void carfield_free_ram(void* ext, size_t size); extern volatile uint32_t last_completed_node_id; extern volatile uint32_t last_task_error_code; -#define GLOBAL_IRQ_ENABLE 0x00001808 +#define GLOBAL_IRQ_ENABLE (1UL << 3) #define EXTERNAL_IRQ_ENABLE 0x00000800 #define PLIC_BASE_ADDRESS 0x04000000 diff --git a/examples/targets/carfield/libs/carfield_lib/src/carfield.c b/examples/targets/carfield/libs/carfield_lib/src/carfield.c index 40d7af4..981ca5c 100644 --- a/examples/targets/carfield/libs/carfield_lib/src/carfield.c +++ b/examples/targets/carfield/libs/carfield_lib/src/carfield.c @@ -115,10 +115,19 @@ static dif_rv_plic_t plic0; void carfield_init_plic() { // Reset PLIC dif_rv_plic_reset(&plic0); + // Set global interrupt enable in CVA6 csr - asm volatile("csrw mstatus, %0\n" : : "r"(GLOBAL_IRQ_ENABLE)); + unsigned long mstatus; + asm volatile ("csrr %0, mstatus" : "=r"(mstatus)); + mstatus |= GLOBAL_IRQ_ENABLE; + asm volatile ("csrw mstatus, %0" :: "r"(mstatus)); + // Set external interrupt enable in CVA6 csr - asm volatile("csrw mie, %0\n" : : "r"(EXTERNAL_IRQ_ENABLE)); + unsigned long mie; + asm volatile ("csrr %0, mie" : "=r"(mie)); + mie |= EXTERNAL_IRQ_ENABLE; + asm volatile ("csrw mie, %0" :: "r"(mie)); + // Setup PLIC mmio_region_t plic_base_addr = mmio_region_from_addr(PLIC_BASE_ADDRESS); dif_result_t t = dif_rv_plic_init(plic_base_addr, &plic0); From 593c607413cf420b29aec603fdd975ce2f815a54 Mon Sep 17 00:00:00 2001 From: Haimrich Date: Tue, 3 Jun 2025 11:05:48 +0200 Subject: [PATCH 2/6] [CARFIELD] fix bug in L2 malloc --- examples/targets/carfield/config/link.ld | 6 +- .../carfield/libs/carfield_lib/src/malloc.c | 94 +++++++++++-------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/examples/targets/carfield/config/link.ld b/examples/targets/carfield/config/link.ld index cd9397a..281ffe1 100644 --- a/examples/targets/carfield/config/link.ld +++ b/examples/targets/carfield/config/link.ld @@ -77,11 +77,9 @@ SECTIONS { *(.bulk.*) } > l2 - .l2_heap : ALIGN(32) { + .l2_heap (NOLOAD) : ALIGN(32) { __l2_heap_start = .; - *(.l2_heap) - *(.l2_heap.*) - . = ALIGN(32); + . = ORIGIN(l2) + LENGTH(l2) - LENGTH(l2_common); __l2_heap_end = .; } > l2 diff --git a/examples/targets/carfield/libs/carfield_lib/src/malloc.c b/examples/targets/carfield/libs/carfield_lib/src/malloc.c index 3b2561e..7273a59 100644 --- a/examples/targets/carfield/libs/carfield_lib/src/malloc.c +++ b/examples/targets/carfield/libs/carfield_lib/src/malloc.c @@ -1,9 +1,13 @@ -/** - * Malloc implementation for bare metal systems using linker-defined heap +/* + * Basic malloc implementation for L2 SPM, + * pointers are uint32_t so that, in theory, + * the same memory pool could be shared between + * 64bit host and 32bit cluster cores. */ #include "carfield_lib/malloc.h" #include "carfield_lib/carfield.h" +#include "carfield_lib/printf.h" #include #include @@ -12,126 +16,142 @@ uint8_t* memory_pool_l2 = &__l2_heap_start; block_header_t* free_list = NULL; + +static size_t l2_heap_size(void) { + return (size_t)(&__l2_heap_end - &__l2_heap_start); +} + +// Offset helpers +static inline uint32_t ptr_to_offset(void* ptr) { + if (!ptr) return 0; + return (uint32_t)((uint8_t*)ptr - memory_pool_l2); +} + +static inline block_header_t* offset_to_ptr(uint32_t offset) { + if (!offset) return NULL; + return (block_header_t*)(memory_pool_l2 + offset); +} + /** * Initialize the memory allocator */ void mem_init_l2(void) { // Create initial free block spanning the entire memory pool free_list = (block_header_t*)memory_pool_l2; - free_list->size = (uint32_t)(&__l2_heap_end - &__l2_heap_start); + free_list->size = (uint32_t)l2_heap_size(); free_list->is_free = 1; free_list->next = 0; + + mini_printf("[MALLOC] L2 memory pool initialized with size %d bytes.\r\n", (int)free_list->size); } /** * Allocate memory of specified size - * + * * @param size Size of memory to allocate in bytes * @return Pointer to allocated memory or NULL if allocation fails */ void* malloc_l2(size_t size) { - carprint("malloc\r\n"); block_header_t *curr, *prev, *new_block; void* result = NULL; - + // Adjust size to include the header and ensure alignment (8-byte in this case) size_t aligned_size = (size + sizeof(block_header_t) + 7) & ~7; - + // Ensure minimum allocation size if (aligned_size < MIN_ALLOC_SIZE + sizeof(block_header_t)) aligned_size = MIN_ALLOC_SIZE + sizeof(block_header_t); - + // Initialize memory pool if not already done if (free_list == NULL) mem_init_l2(); - + // First-fit search for a free block prev = NULL; curr = free_list; - + while (curr != NULL) { if (curr->is_free && curr->size >= aligned_size) { // Found a suitable block - + // Split the block if it's significantly larger than requested if (curr->size >= aligned_size + sizeof(block_header_t) + MIN_ALLOC_SIZE) { new_block = (block_header_t*)((uint8_t*)curr + aligned_size); new_block->size = curr->size - aligned_size; new_block->is_free = 1; new_block->next = curr->next; - + curr->size = aligned_size; - curr->next = (uint32_t)new_block; + curr->next = ptr_to_offset(new_block); } - + // Mark block as allocated curr->is_free = 0; - + // Return pointer to usable memory (after header) result = (void*)((uint8_t*)curr + sizeof(block_header_t)); break; } - + prev = curr; - curr = (block_header_t*)curr->next; + curr = offset_to_ptr(curr->next); } - + + mini_printf("[MALLOC] Allocated %d bytes block in L2 at %p.\r\n", (int)size, result); return result; } /** * Free previously allocated memory - * + * * @param ptr Pointer to memory to free */ void free_l2(void* ptr) { block_header_t *block, *next, *prev; - + if (ptr == NULL) return; - + // Get the block header from the pointer block = (block_header_t*)((uint8_t*)ptr - sizeof(block_header_t)); - + // Sanity check - ensure the pointer is within our heap - if ((uint8_t*)block < memory_pool_l2 || - (uint8_t*)block >= memory_pool_l2 + (&__l2_heap_end - &__l2_heap_start)) + if ((uint8_t*)block < memory_pool_l2 || + (uint8_t*)block >= memory_pool_l2 + l2_heap_size()) return; // Ignore attempts to free memory outside our heap - + // Mark block as free block->is_free = 1; - + // Coalesce with adjacent free blocks - + // Find the previous block prev = NULL; next = free_list; while (next != NULL && next < block) { prev = next; - next = (block_header_t*)next->next; + next = offset_to_ptr(next->next); } - + // Merge with next block if adjacent and free - if ((uint8_t*)block + block->size == (uint8_t*)next && next->is_free) { + if (next && ((uint8_t*)block + block->size == (uint8_t*)next) && next->is_free) { block->size += next->size; block->next = next->next; } else { - block->next = (uint32_t)next; + block->next = ptr_to_offset(next); } - + // Merge with previous block if adjacent and free - if (prev != NULL && (uint8_t*)prev + prev->size == (uint8_t*)block && prev->is_free) { + if (prev && ((uint8_t*)prev + prev->size == (uint8_t*)block) && prev->is_free) { prev->size += block->size; prev->next = block->next; - } else if (prev != NULL) { - prev->next = (uint32_t)block; + } else if (prev) { + prev->next = ptr_to_offset(block); } else { free_list = block; } } -// [calloc and realloc implementations remain the same as before] - void* malloc(size_t size) { return malloc_l2(size); From 0059f459d3c311e11eba15282fc1faf77f4d7de2 Mon Sep 17 00:00:00 2001 From: Mohamed Amine Hamdi Date: Tue, 3 Jun 2025 11:08:21 +0200 Subject: [PATCH 3/6] [PULP] fix cost model --- match/cost_model/examples/pulp_cluster.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/match/cost_model/examples/pulp_cluster.py b/match/cost_model/examples/pulp_cluster.py index 75a27d4..e7c2ea3 100644 --- a/match/cost_model/examples/pulp_cluster.py +++ b/match/cost_model/examples/pulp_cluster.py @@ -94,8 +94,10 @@ def adjust_temporal_mapping(self, temporal_mapping_dict, operand_list, layer): min_innermost_loops=min([len(temporal_mapping_dict[operand][0]) for operand in operand_list]) new_innermost_loops=min_innermost_loops max_tile_found=False + c_k_mapping = "C" in layer.layer_attrs["operand_source_dimension_mapping"]["I"] and layer.layer_attrs["operand_source_dimension_mapping"]["I"]["C"]=="K" + ACCEPTED_UNEVEN_TILE_DIMENSIONS_ACT_OUT = ("K", "C") if not c_k_mapping else ("C",) for idx in range(min_innermost_loops, len(temporal_mapping_dict["I"][0])): - if (not max_tile_found) and (temporal_mapping_dict["I"][0][idx][0] in self.ACCEPTED_UNEVEN_TILE_DIMENSIONS_ACT_OUT): + if (not max_tile_found) and (temporal_mapping_dict["I"][0][idx][0] in ACCEPTED_UNEVEN_TILE_DIMENSIONS_ACT_OUT): new_innermost_loops=idx+1 else: max_tile_found = True From d94cf351713011852a8f7538baf7337e21b37987 Mon Sep 17 00:00:00 2001 From: Haimrich Date: Tue, 3 Jun 2025 11:12:35 +0200 Subject: [PATCH 4/6] [CARFIELD] add test fp16 network --- .../carfield/model_fp16/model_graph.relay | 14 ++++++++ .../carfield/model_fp16/model_params.txt | Bin 0 -> 37721 bytes examples/targets/carfield/run_fp.py | 33 ++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 examples/targets/carfield/model_fp16/model_graph.relay create mode 100644 examples/targets/carfield/model_fp16/model_params.txt create mode 100644 examples/targets/carfield/run_fp.py diff --git a/examples/targets/carfield/model_fp16/model_graph.relay b/examples/targets/carfield/model_fp16/model_graph.relay new file mode 100644 index 0000000..420db91 --- /dev/null +++ b/examples/targets/carfield/model_fp16/model_graph.relay @@ -0,0 +1,14 @@ +#[version = "0.0.5"] +def @main(%input_0: Tensor[(1, 3), float16], %dense_1_weights: Tensor[(3872, 3), float16], %dense_1_bias: Tensor[(3872), float16], %conv_weights: Tensor[(3, 8, 3, 3), float16], %conv_bias: Tensor[(3), float16], %dense_2_weights: Tensor[(8, 363), float16], %dense_2_bias: Tensor[(8), float16]) { + %0 = nn.dense(%input_0, %dense_1_weights, units=None, out_dtype="float16"); + %1 = nn.bias_add(%0, %dense_1_bias, axis=-1); + %2 = nn.relu(%1); + %3 = reshape(%2, newshape=[1, 8, 22, 22]); + %4 = nn.conv2d(%3, %conv_weights, strides=[2, 2], padding=[1, 1, 1, 1], kernel_size=[3, 3]); + %5 = nn.bias_add(%4, %conv_bias); + %6 = nn.relu(%5); + %7 = reshape(%6, newshape=[1, 363]); + %8 = nn.dense(%7, %dense_2_weights, units=None, out_dtype="float16"); + %9 = nn.bias_add(%8, %dense_2_bias, axis=-1); + nn.relu(%9) +} diff --git a/examples/targets/carfield/model_fp16/model_params.txt b/examples/targets/carfield/model_fp16/model_params.txt new file mode 100644 index 0000000000000000000000000000000000000000..487f6dbb83b8d83973000b43e85a2581099000f0 GIT binary patch literal 37721 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 au@P+{JB*lMEm+RuXgB$iFi1=X#rVI{0|*HK literal 0 HcmV?d00001 diff --git a/examples/targets/carfield/run_fp.py b/examples/targets/carfield/run_fp.py new file mode 100644 index 0000000..c13eadb --- /dev/null +++ b/examples/targets/carfield/run_fp.py @@ -0,0 +1,33 @@ +import sys + +MATCH_PATH = "../../../.." +sys.path.append(f"{MATCH_PATH}/match/match-tvm/python") +sys.path.append(f"{MATCH_PATH}/match/zigzag") +sys.path.append(f"{MATCH_PATH}/match") +sys.path.append(".") + +import match +from match.utils.utils import get_default_inputs +from match.model.model import MatchModel +from carfield import Carfield + +INPUT_FILE_PATH = "model_fp/input.txt" +RELAY_FILE_PATH = "model_fp16/model_graph.relay" +RELAY_PARAMS_PATH = "model_fp16/model_params.txt" +OUTPUT_DIR = "output_fp" + +relay_mod, relay_params = match.get_relay_network(input_type="relay", filename=RELAY_FILE_PATH, params_filename=RELAY_PARAMS_PATH) + +oenne_model = MatchModel( + relay_mod = relay_mod, + relay_params = relay_params, + model_name = "model", + default_inputs = get_default_inputs(mod=relay_mod, params=relay_params, input_files=[INPUT_FILE_PATH]), + #handle_out_fn="handle_int_classifier", + debug=True +) +match.match( + model = oenne_model, + target = Carfield(), + output_path = OUTPUT_DIR, +) \ No newline at end of file From 1db145ae332db42db77b6b32560ba2ceb5c7b5be Mon Sep 17 00:00:00 2001 From: Haimrich Date: Tue, 3 Jun 2025 11:13:16 +0200 Subject: [PATCH 5/6] [CARFIELD] add fp support in mini_printf --- .../carfield/libs/carfield_lib/src/printf.c | 103 ++++++++++++------ 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/examples/targets/carfield/libs/carfield_lib/src/printf.c b/examples/targets/carfield/libs/carfield_lib/src/printf.c index 1b925cd..7dd3bfd 100644 --- a/examples/targets/carfield/libs/carfield_lib/src/printf.c +++ b/examples/targets/carfield/libs/carfield_lib/src/printf.c @@ -3,10 +3,11 @@ #include #include #include +#include #include "carfield_lib/uart.h" -// Convert integers to strings with support for different sizes + void mini_itoa(int value, char *str, int base) { char *ptr = str, *ptr1 = str, tmp_char; int tmp_value; @@ -32,7 +33,7 @@ void mini_itoa(int value, char *str, int base) { } } -// Convert unsigned long integers to strings (for pointers) + void mini_ultoa(unsigned long value, char *str, int base) { char *ptr = str, *ptr1 = str, tmp_char; unsigned long tmp_value; @@ -53,15 +54,61 @@ void mini_ultoa(unsigned long value, char *str, int base) { } +static void mini_ftoa(double f, char *buf, int precision) { + if (isnan(f)) { + buf[0] = 'n'; buf[1] = 'a'; buf[2] = 'n'; buf[3] = '\0'; + return; + } + if (isinf(f)) { + if (f < 0) { + buf[0] = '-'; buf[1] = 'i'; buf[2] = 'n'; buf[3] = 'f'; buf[4] = '\0'; + } else { + buf[0] = 'i'; buf[1] = 'n'; buf[2] = 'f'; buf[3] = '\0'; + } + return; + } + if (f < 0) { + *buf++ = '-'; + f = -f; + } + unsigned long ipart = (unsigned long)f; + double fpart = f - (double)ipart; + + // Integer part + char tmp[20]; + mini_ultoa(ipart, tmp, 10); + char *p = tmp; + while (*p) *buf++ = *p++; + + // Decimal point and fractional part + if (precision > 0) { + *buf++ = '.'; + // Multiply out for specified precision, round correctly + double rounding = 0.5; + for (int i = 0; i < precision; ++i) + rounding /= 10.0; + fpart += rounding; + + for (int i = 0; i < precision; ++i) { + fpart *= 10.0; + int digit = (int)fpart; + *buf++ = '0' + digit; + fpart -= digit; + } + } + *buf = '\0'; +} + + size_t mini_vsnprintf(char *out, size_t n, const char *fmt, va_list args) { char *out_ptr = out; size_t remaining = n; - if (n == 0) return 0; // Handle zero-sized buffer + if (n == 0) return 0; - char buffer[20]; // Increased to handle 64-bit pointers (16 hex digits + null terminator) - - while (*fmt && remaining > 1) { // Keep space for null terminator + char buffer[32]; + + while (*fmt && remaining > 1) { if (*fmt == '%') { fmt++; if (*fmt == 'd' || *fmt == 'i') { @@ -73,13 +120,12 @@ size_t mini_vsnprintf(char *out, size_t n, const char *fmt, va_list args) { } } else if (*fmt == 's') { char *str = va_arg(args, char*); - if (str) { // Check for NULL pointer + if (str) { while (*str && remaining > 1) { *out_ptr++ = *str++; remaining--; } } else { - // Handle NULL string const char *null_str = "NULL"; for (const char *p = null_str; *p && remaining > 1; p++) { *out_ptr++ = *p; @@ -89,7 +135,6 @@ size_t mini_vsnprintf(char *out, size_t n, const char *fmt, va_list args) { } else if (*fmt == 'x') { unsigned int val = va_arg(args, unsigned int); mini_itoa(val, buffer, 16); - // Only add 0x prefix if there's room if (remaining > 2) { *out_ptr++ = '0'; remaining--; *out_ptr++ = 'x'; remaining--; @@ -99,70 +144,59 @@ size_t mini_vsnprintf(char *out, size_t n, const char *fmt, va_list args) { } } } else if (*fmt == 'p' || *fmt == 'P') { - // Handle pointer type with proper casting void *ptr = va_arg(args, void*); if (ptr == NULL) { - // Handle NULL pointer const char *null_ptr = "NULL"; for (const char *p = null_ptr; *p && remaining > 1; p++) { *out_ptr++ = *p; remaining--; } } else { - // Convert pointer to hex representation with proper size unsigned long ptr_val = (unsigned long)ptr; mini_ultoa(ptr_val, buffer, 16); - - // Add leading zeros to ensure consistent width + int len = 0; for (char *p = buffer; *p; p++) len++; - - // Add 0x prefix and pad with zeros if there's room + if (remaining > 2) { *out_ptr++ = '0'; remaining--; *out_ptr++ = 'x'; remaining--; - - // Add padding zeros for consistent pointer width - // For 32-bit: 8 hex digits, For 64-bit: 16 hex digits int target_width; - - // Determine if we're using a 32-bit or 64-bit pointer int is_64bit = 0; #if defined(__LP64__) || defined(_LP64) || defined(__x86_64__) || defined(_M_X64) is_64bit = 1; #endif - - // When using %p, print as 32-bit (8 hex digits) - // When using %P, print as 64-bit (16 hex digits) - // Or when using %p but the pointer requires 64-bit representation if (*fmt == 'P' || (is_64bit && len > 8)) { - target_width = 16; // 64-bit format + target_width = 16; } else { - target_width = 8; // 32-bit format + target_width = 8; } - int padding = target_width - len; while (padding > 0 && remaining > 1) { *out_ptr++ = '0'; remaining--; padding--; } - - // Add the actual hex digits for (char *p = buffer; *p && remaining > 1; p++) { *out_ptr++ = *p; remaining--; } } } + } else if (*fmt == 'f') { + // Default: 6 decimal places + double val = va_arg(args, double); + mini_ftoa(val, buffer, 6); + for (char *p = buffer; *p && remaining > 1; p++) { + *out_ptr++ = *p; + remaining--; + } } else if (*fmt == '%') { - // Handle %% escape if (remaining > 1) { *out_ptr++ = '%'; remaining--; } } else { - // Unknown specifier, print as is if (remaining > 1) { *out_ptr++ = '%'; remaining--; } @@ -176,9 +210,8 @@ size_t mini_vsnprintf(char *out, size_t n, const char *fmt, va_list args) { } fmt++; } - *out_ptr = '\0'; // Null-terminate - - return n - remaining; // Return number of characters written (not including null terminator) + *out_ptr = '\0'; + return n - remaining; } From 0fad0e043a94a6ea06f44acbb1a1125877f7d751 Mon Sep 17 00:00:00 2001 From: Haimrich Date: Tue, 3 Jun 2025 11:14:14 +0200 Subject: [PATCH 6/6] [CARFIELD] add naive fp16 linear kernel for pulp cluster --- .../libs/carfield_lib/include/cluster.h | 6 ++ .../carfield/libs/carfield_lib/src/cluster.c | 56 ++++++++++++++++++- .../include/pulp_nn_kernels_fp16.h | 14 +++++ .../pulp_nn_fp16/src/pulp_nn_linear_fp16.c | 37 ++++++++++++ examples/targets/carfield/pulp_cluster.py | 16 +++++- 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 examples/targets/carfield/libs/pulp_nn_fp16/include/pulp_nn_kernels_fp16.h create mode 100644 examples/targets/carfield/libs/pulp_nn_fp16/src/pulp_nn_linear_fp16.c diff --git a/examples/targets/carfield/libs/carfield_lib/include/cluster.h b/examples/targets/carfield/libs/carfield_lib/include/cluster.h index e6aa955..7f8230c 100644 --- a/examples/targets/carfield/libs/carfield_lib/include/cluster.h +++ b/examples/targets/carfield/libs/carfield_lib/include/cluster.h @@ -14,6 +14,10 @@ #include "pulp.h" #include "bench/bench.h" #include "pulp_nn/pulp_nn_kernels.h" + +typedef float16 fp16; +typedef fp16 v2f16 __attribute__((vector_size (4))); + #endif @@ -76,6 +80,8 @@ void pulp_nn_hoparallel_conv2d_wrapper(MatchCtx* ctx); void pulp_nn_add_wrapper(MatchCtx* ctx); +void pulp_nn_dense_fp16_wrapper(MatchCtx* ctx); + void pulp_nn_wrapper(MatchCtx* ctx); #endif // CAR_LIB_CLUSTER_H \ No newline at end of file diff --git a/examples/targets/carfield/libs/carfield_lib/src/cluster.c b/examples/targets/carfield/libs/carfield_lib/src/cluster.c index 3078f1e..cb8775b 100644 --- a/examples/targets/carfield/libs/carfield_lib/src/cluster.c +++ b/examples/targets/carfield/libs/carfield_lib/src/cluster.c @@ -5,6 +5,9 @@ #include "carfield_lib/mbox.h" #include "carfield_lib/utils.h" +#include "pulp_nn/pulp_nn_kernels.h" +#include "pulp_nn_fp16/pulp_nn_kernels_fp16.h" + //#define CLUSTER_LIB_DEBUG #define DEBUG_CALLOC_L1_SCRATCHPAD 0 #define DEBUG_BLOCKING_DMA 0 @@ -58,6 +61,11 @@ void cluster_sync_cores(MatchCtx* ctx) void cluster_lib_init(MatchCtx* ctx) { + #ifdef CLUSTER_LIB_DEBUG + for (int i = 0; i < 20000; i++) { + asm volatile("fence rw,rw":::"memory"); + } + #endif dma_transfer_ = dma_transfer_create(); #ifdef CLUSTER_LIB_DEBUG mini_printf("[PULP] Yo! Cluster is alive! DMA counter is %d\r\n", dma_transfer_); @@ -719,6 +727,36 @@ void pulp_nn_add_wrapper(MatchCtx* ctx){ ); } + +void pulp_nn_dense_fp16_wrapper(MatchCtx* ctx) { + MatchTensor* tensors = ctx->tensors->tensors; + int num_ops = ctx->ops->num_ops; + int num_tensors = ctx->tensors->num_tensors; + int out_ch = tensors[num_tensors-1].tiles[L1_SCRATCHPAD*2+1].size; + int inp_ch = tensors[0].tiles[L1_SCRATCHPAD*2+1].size; + #ifdef CLUSTER_LIB_DEBUG + if(rt_core_id() == 0) { + mini_printf("[PULP][KER] pulp_nn_linear_fp16: "); + mini_printf("Out. tile (%d,) | ", out_ch); + mini_printf("Inp. tile (%d,)\r\n", inp_ch); + } + #endif + pulp_nn_linear_fp16( + // activations pt + (float16*)tensors[0].pt, // acts pt + // weights pt + (float16*)tensors[1].pt, // weights pt + // output pt + (float16*)tensors[num_tensors-1].pt, // output pt + // bias pt + num_tensors>4 ? (float16*)NULL : (float16*)tensors[2].pt, // bias pt + // dims + inp_ch, + out_ch + ); +} + + void pulp_nn_wrapper(MatchCtx* ctx){ switch(ctx->pattern_name){ @@ -728,9 +766,9 @@ void pulp_nn_wrapper(MatchCtx* ctx){ case conv2d: pulp_nn_hoparallel_conv2d_wrapper(ctx); break; - case dense_out: - pulp_nn_dense_out_int_wrapper(ctx); - break; + //case dense_out: + // pulp_nn_dense_out_int_wrapper(ctx); + // break; // case pulp_nn_dw_conv2d_less_4_pattern: // pi_team_offload_preset(pulp_nn_dw_conv2d_less_4_wrapper, ctx); // break; @@ -743,6 +781,8 @@ void pulp_nn_wrapper(MatchCtx* ctx){ case add_requant: pulp_nn_add_wrapper(ctx); break; + case dense_fp16: + pulp_nn_dense_fp16_wrapper(ctx); default: break; } @@ -807,4 +847,14 @@ uint32_t cluster_timer_stop() { } + +double __attribute__((weak)) __extendhfdf2(float16 val) +{ + float res; + __asm__ __volatile__ ("fcvt.s.h %0, %1": "=f"(res): "f"(val) :); + return (double) res; +} + + + #endif \ No newline at end of file diff --git a/examples/targets/carfield/libs/pulp_nn_fp16/include/pulp_nn_kernels_fp16.h b/examples/targets/carfield/libs/pulp_nn_fp16/include/pulp_nn_kernels_fp16.h new file mode 100644 index 0000000..1d70f7b --- /dev/null +++ b/examples/targets/carfield/libs/pulp_nn_fp16/include/pulp_nn_kernels_fp16.h @@ -0,0 +1,14 @@ +#ifdef __pulp_cluster__ + +#include + +void pulp_nn_linear_fp16( + float16 *__restrict__ input, + float16 *__restrict__ weight, + float16 *__restrict__ output, + float16 *__restrict__ bias, + uint32_t dim_i, + uint32_t dim_o +); + +#endif \ No newline at end of file diff --git a/examples/targets/carfield/libs/pulp_nn_fp16/src/pulp_nn_linear_fp16.c b/examples/targets/carfield/libs/pulp_nn_fp16/src/pulp_nn_linear_fp16.c new file mode 100644 index 0000000..3852fed --- /dev/null +++ b/examples/targets/carfield/libs/pulp_nn_fp16/src/pulp_nn_linear_fp16.c @@ -0,0 +1,37 @@ +#ifdef __pulp_cluster__ + +#include "pulp_nn_fp16/pulp_nn_kernels_fp16.h" + +#include + +#include + +#define log2(x) __builtin_pulp_fl1(x) +#define min(a,b) ((a)<(b)?(a):(b)) + + +void pulp_nn_linear_fp16( + float16 *__restrict__ input, + float16 *__restrict__ weight, + float16 *__restrict__ output, + float16 *__restrict__ bias, + uint32_t dim_i, + uint32_t dim_o +) +{ + const int NUM_CORES = get_core_num(); + + int chunk = (dim_o >> log2(NUM_CORES)) + ((dim_o & (NUM_CORES - 1)) != 0); + int start = min(chunk * rt_core_id(), dim_o); + int stop = min(start + chunk, dim_o); + + for (int j = start; j < stop; j++) { + float16 sum = bias ? bias[j] : 0; + for (int k = 0; k < dim_i; k++) { + sum += input[k] * weight[j * dim_i + k]; + } + output[j] = sum; + } +} + +#endif \ No newline at end of file diff --git a/examples/targets/carfield/pulp_cluster.py b/examples/targets/carfield/pulp_cluster.py index f679f2a..f1ae5f3 100644 --- a/examples/targets/carfield/pulp_cluster.py +++ b/examples/targets/carfield/pulp_cluster.py @@ -9,7 +9,7 @@ from match.cost_model.examples.pulp_cluster import PulpClusterCostModel from match.target.memory_inst import MemoryInst from match.tensor.tensor import MatchTensor -from tvm.relay.dataflow_pattern import wildcard, is_op, is_constant +from tvm.relay.dataflow_pattern import wildcard, is_op, is_constant, has_dtype from match.partition.partitioning_pattern import PartitioningPattern class PulpCluster(ExecModule): @@ -19,6 +19,7 @@ def __init__(self, num_cores: int=8, l1_kb_size: int=64, l2_kb_size: int=512, libs_required={ "carfield_lib": ModuleLib(name="carfield_lib", base_path=os.path.dirname(__file__)+"/libs/carfield_lib"), "pulp_nn": ModuleLib(name="pulp_nn", base_path=os.path.dirname(__file__)+"/libs/pulp_nn"), + "pulp_nn_fp16": ModuleLib(name="pulp_nn", base_path=os.path.dirname(__file__)+"/libs/pulp_nn_fp16"), }) self.NUM_CORES = num_cores self.L1_SCRATCHPAD_KB_SIZE = l1_kb_size @@ -188,6 +189,11 @@ def dense_pt_out(): add = is_op("add")(dense, is_constant()) | is_op("add")(is_op("cast")(dense),is_constant()) return add + def dense_fp16(): + dense = is_op("nn.dense")(wildcard(), wildcard()) + dense_add = is_op("add")(dense, is_constant()) + return dense_add + def add_pt_requant(): cast_a = is_op("cast")(wildcard()) cast_b = is_op("cast")(wildcard()) @@ -201,6 +207,11 @@ def add_pt_requant(): def only_out_uint8(node): return add_checks_get_first_op(node, "cast").attrs.dtype=="uint8" + + def only_out_fp16(node): + is_fp16 = add_checks_get_first_op(node, "nn.dense").attrs.out_dtype == "float16" + is_fp16 |= getattr(node.attrs, "out_dtype", None) == "float16" + return is_fp16 def only_std_convs(node): conv = add_checks_get_first_op(node, "nn.conv2d") @@ -239,7 +250,8 @@ def only_dw_convs(node): return True return [ - PartitioningPattern(name="dense_out",pattern=dense_pt_out), + #PartitioningPattern(name="dense_out",pattern=dense_pt_out), + PartitioningPattern(name="dense_fp16",pattern=dense_fp16,additional_checks=only_out_fp16), PartitioningPattern(name="dense",pattern=dense_pt_requant,additional_checks=only_out_uint8), PartitioningPattern(name="conv2d",pattern=conv_pt_requant,additional_checks=only_std_convs), PartitioningPattern(name="depthwise_conv2d",pattern=conv_pt_requant,additional_checks=only_dw_convs),