diff --git a/Deeploy/Targets/GAP9/DMA/MchanDma.py b/Deeploy/Targets/GAP9/DMA/MchanDma.py index defda36d..2906c2af 100644 --- a/Deeploy/Targets/GAP9/DMA/MchanDma.py +++ b/Deeploy/Targets/GAP9/DMA/MchanDma.py @@ -3,9 +3,9 @@ # SPDX-License-Identifier: Apache-2.0 import math -from typing import Dict, Tuple +from typing import Dict, List, Tuple -from Deeploy.DeeployTypes import NetworkContext, NodeTemplate, OperatorRepresentation, VariableBuffer +from Deeploy.DeeployTypes import CodeSnippet, NetworkContext, NodeTemplate, OperatorRepresentation, VariableBuffer from Deeploy.TilingExtension.AsyncDma import AsyncDma, DmaDirection, Future, PerTensorWaitingStrategy @@ -36,6 +36,23 @@ class GAP9MchanDma(AsyncDma): "{ mchan_transfer_t __mchan_tmp = { .cmd = ${cmd}, .size = ${size}, .loc = ${loc}, .ext = ${ext}, .ext_size_1d = ${size_1d}, .ext_stride_1d = ${stride_2d} }; mchan_transfer_push_2d(__mchan_tmp); }" ), } + # Chunked-1D fallback. `loc`/`ext` stay raw buffer identifiers so the dynamic + # reference extractor in Closure.extractDynamicReferences can pick them up and + # propagate them through closure args; byte offsets are applied in the C + # expression instead of being baked into the identifier. + _chunkedTransferTemplate = NodeTemplate("{ mchan_transfer_t __mchan_tmp = { .cmd = ${cmd}, .size = ${size}, " + ".loc = (void *)((char *)${loc} + ${loc_offset}), " + ".ext = (void *)((char *)${ext} + ${ext_offset}) }; " + "mchan_transfer_push_1d(__mchan_tmp); }") + + # The mchan cmd size field is 17 bits, so it holds 0..(2^17 - 1) = 131071. + # Using 1<<17 as the per-chunk max makes chunkSize carry into bit 17 and + # clobber the direction/INC/event flags above it, producing a size-0 DMA whose + # completion is scheduled at the current sim time -- which trips gvsoc's + # "Time must be higher than current time". Same constant and same reason as + # the PULPOpen backend. + _MAX_1D_TRANSFER_BYTES = (1 << 17) - 1 # 131071 bytes + # PerTensor, NOT Direction: GAP9 mchan allocates a fresh channel on every # descriptor enqueue (each mchan_transfer_push_* writes a new descriptor, and # the hardware advances to the next channel). DirectionWaitingStrategy shares @@ -85,10 +102,12 @@ def transferOpRepr(self, externalBuffer: VariableBuffer, localBuffer: VariableBu mchanFlags += (1 << 3) # event enable mchanTransferSize = math.prod(shape) - mchanTransferSizeBits = math.ceil(math.log2(mchanTransferSize)) if mchanTransferSize > 0 else 0 - assert mchanTransferSizeBits <= 17, ( - "The transfer size is not representable with 17 bits. " - f"Received transfer size {mchanTransferSize} that requires {mchanTransferSizeBits} bits") + # <=, not a bit-count: ceil(log2(131072)) is 17 and would pass, yet 131072 + # does not fit a 17-bit field -- it carries into the flag bits. + assert mchanTransferSize <= self._MAX_1D_TRANSFER_BYTES, ( + f"Transfer size {mchanTransferSize} exceeds the 17-bit mchan limit of " + f"{self._MAX_1D_TRANSFER_BYTES} B. 1D transfers are chunked in transfer(); " + "a 2D transfer this large needs the same treatment.") # cmd = (flags << 17) + size, matching PULPOpen MchanDma pattern operatorRepresentation["cmd"] = (mchanFlags << 17) + mchanTransferSize @@ -99,3 +118,39 @@ def transferOpRepr(self, externalBuffer: VariableBuffer, localBuffer: VariableBu operatorRepresentation["stride_2d"] = strideExt[0] return operatorRepresentation + + def transfer(self, ctxt: NetworkContext, externalBuffer: VariableBuffer, localBuffer: VariableBuffer, + shape: Tuple[int, ...], strideExt: Tuple[int, ...], strideLoc: Tuple[int, ...], + direction: DmaDirection, future: Future) -> List[CodeSnippet]: + """Split 1D transfers that exceed the 17-bit mchan size field. + + Tiled deployments never reach the limit because a tile is small, so this + only fires for whole-tensor moves -- an untiled configuration, or a model + whose tensor happens to exceed 128 KB. Without it those runs die at + codegen with "transfer size is not representable with 17 bits" (ResNet-8 + 147,456 B, CCT 262,144 B). The PULPOpen backend has had this since its + own untiled work; GAP9 did not. + """ + totalSize = math.prod(shape) + if len(shape) == 1 and totalSize > self._MAX_1D_TRANSFER_BYTES: + mchanFlags = 0 + mchanFlags += (1 << 0) if direction == "ExternalToLocal" else 0 + mchanFlags += (1 << 1) # increment addresses + mchanFlags += (1 << 3) # event enable + chunks: List[CodeSnippet] = [] + offset = 0 + while offset < totalSize: + chunkSize = min(self._MAX_1D_TRANSFER_BYTES, totalSize - offset) + opRepr: OperatorRepresentation = { + "loc": localBuffer.name, + "ext": externalBuffer.name, + "loc_offset": offset, + "ext_offset": offset, + "future": future.name, + "cmd": (mchanFlags << 17) + chunkSize, + "size": chunkSize, + } + chunks.append(CodeSnippet(self._chunkedTransferTemplate, opRepr)) + offset += chunkSize + return chunks + return super().transfer(ctxt, externalBuffer, localBuffer, shape, strideExt, strideLoc, direction, future) diff --git a/Deeploy/Targets/GAP9/Templates/AllocateTemplate.py b/Deeploy/Targets/GAP9/Templates/AllocateTemplate.py index b81c74eb..46429295 100644 --- a/Deeploy/Targets/GAP9/Templates/AllocateTemplate.py +++ b/Deeploy/Targets/GAP9/Templates/AllocateTemplate.py @@ -17,8 +17,18 @@ # it, and the run continues until some load faults at a garbage address -- or # worse, silently reads whatever is mapped there. Abort at the point of failure # instead, so the log names the cause rather than an unrelated Invalid access. +# DEEPLOY_L1_AS_L2 (the untiled baseline): allocate what the tiler calls an "L1" +# buffer out of FC L2 instead. The cluster then computes on L2 addresses directly +# and no tile is ever staged, which is what "untiled" means -- keeping L1 and +# only widening L2 still tiles, just from a bigger source. PR #21 reached the +# same end state by running sed over the generated C; emitting both branches and +# letting the preprocessor choose keeps one codegen valid for both builds. gap9L1AllocateTemplate = NodeTemplate( + "#ifdef DEEPLOY_L1_AS_L2\n" + "${name} = (${type.typeName}) pi_l2_malloc(sizeof(${type.referencedType.typeName}) * ${size});\n" + "#else\n" "${name} = (${type.typeName}) pi_l1_malloc((void *) 0, sizeof(${type.referencedType.typeName}) * ${size});\n" + "#endif\n" "if (${name} == NULL) {\n" " printf(\"FATAL: pi_l1_malloc failed for ${name} (%u B) -- cluster L1 exhausted; " "lower --l1 or the cluster stacks\\r\\n\", (unsigned)(sizeof(${type.referencedType.typeName}) * ${size}));\n" @@ -28,8 +38,11 @@ gap9L2GlobalInitTemplate = NodeTemplate( "static PI_L2 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n") -gap9L1GlobalInitTemplate = NodeTemplate( - "static PI_L1 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n") +gap9L1GlobalInitTemplate = NodeTemplate("#ifdef DEEPLOY_L1_AS_L2\n" + "static PI_L2 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n" + "#else\n" + "static PI_L1 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n" + "#endif\n") gap9L2GlobalAllocateTemplate = NodeTemplate("") @@ -67,7 +80,7 @@ % if _memoryLevel == "L1": ${name} = (${type.typeName}) pi_l1_malloc((void *) 0, sizeof(${type.referencedType.typeName}) * ${size}); if (${name} == NULL) { - printf("FATAL: pi_l1_malloc failed for ${name} (%u B) -- cluster L1 exhausted; lower --l1 or the cluster stacks\\r\\n", (unsigned)(sizeof(${type.referencedType.typeName}) * ${size})); + printf("FATAL: arena allocation failed for ${name} (%u B) -- cluster L1 exhausted; lower --l1 or the cluster stacks\\r\\n", (unsigned)(sizeof(${type.referencedType.typeName}) * ${size})); return; }\n % elif _memoryLevel == "L2" or _memoryLevel is None: diff --git a/DeeployTest/CMakeLists.txt b/DeeployTest/CMakeLists.txt index a7980cdd..c242b1dc 100644 --- a/DeeployTest/CMakeLists.txt +++ b/DeeployTest/CMakeLists.txt @@ -6,6 +6,17 @@ include_directories(${GENERATED_SOURCE}) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# Untiled baseline (see Platforms/GAP9/CMakeLists.txt). The define must be in +# scope for the *generated* network code and for deeploylib, not just for the +# test executable: the L1 allocations and the mchan calls it switches live +# there. Directory scope here covers every target created below. +if(DEEPLOY_L1_AS_L2) + add_compile_definitions(DEEPLOY_L1_AS_L2) + if(TARGET deeploylib) + target_compile_definitions(deeploylib INTERFACE DEEPLOY_L1_AS_L2) + endif() +endif() + if(TRAINING) add_library(training_network OBJECT ${GENERATED_SOURCE}/TrainingNetwork.c) target_link_libraries(training_network PUBLIC deeploylib) diff --git a/DeeployTest/Platforms/GAP9/CMakeLists.txt b/DeeployTest/Platforms/GAP9/CMakeLists.txt index 1be6dcad..bdf47ee4 100644 --- a/DeeployTest/Platforms/GAP9/CMakeLists.txt +++ b/DeeployTest/Platforms/GAP9/CMakeLists.txt @@ -64,6 +64,14 @@ if(SLAVESTACKSIZE) target_compile_definitions(${ProjectId} PRIVATE SLAVESTACKSIZE=${SLAVESTACKSIZE}) endif() +# Untiled baseline: "L1" buffers come from FC L2 and the mchan transfers become +# memcpy, so the cluster computes over the L2 fabric instead of out of TCDM. +# Both branches are emitted by codegen; this define selects the untiled one. +if(DEEPLOY_L1_AS_L2) + target_compile_definitions(${ProjectId} PRIVATE DEEPLOY_L1_AS_L2) + message(STATUS "[cMake ] DEEPLOY_L1_AS_L2 = ON (untiled: compute from L2)") +endif() + # RW: Waive sign comparison warnings from pulp_nn_utils.h target_compile_options(${NETWORK_LIB} PRIVATE -Wno-sign-compare diff --git a/TargetLibraries/GAP9/inc/mchan.h b/TargetLibraries/GAP9/inc/mchan.h index 085f20b2..ea0db7a0 100644 --- a/TargetLibraries/GAP9/inc/mchan.h +++ b/TargetLibraries/GAP9/inc/mchan.h @@ -83,6 +83,71 @@ typedef struct { int ext_stride_1d; } mchan_transfer_t; +#ifdef DEEPLOY_L1_AS_L2 + +/* Untiled baseline. With DEEPLOY_L1_AS_L2 the "L1" buffers are allocated out of + * FC L2 (see gap9L1AllocateTemplate), so source and destination are both L2 and + * there is nothing for the DMA to do. It cannot be asked to do it either: the + * mchan hardware ignores the destination address and routes `loc` into the + * cluster L1 banks through the low bits, so a real transfer would write into L1 + * and leave the L2 destination untouched -- the out-of-range L1 bank requests + * and computed=0.0 that PR #21 first ran into. + * + * Replace the transfers with memcpy and make the channel API inert. Unlike + * PR #21 the 2D variant is provided as well, so a model whose tiler emits + * strided transfers is handled rather than silently skipped. + * + * The cost this measures is real: the cluster reaches these buffers over the L2 + * fabric instead of out of 1-cycle TCDM. */ + +#include + +static int mchan_transfer_get_id() { return 0; } + +static void mchan_transfer_push_1d(mchan_transfer_t trans) { + uint32_t size = (uint32_t)trans.cmd & ((1u << MCHAN_TRANSFER_LEN_SIZE) - 1); + if (trans.cmd & MCHAN_CMD_FLAG_DIRECTION_EXT2LOC) { + memcpy(trans.loc, trans.ext, size); + } else { + memcpy(trans.ext, trans.loc, size); + } +} + +static void mchan_transfer_push_2d(mchan_transfer_t trans) { + uint32_t total = (uint32_t)trans.cmd & ((1u << MCHAN_TRANSFER_LEN_SIZE) - 1); + uint32_t line = (uint32_t)trans.ext_size_1d; + uint32_t stride = (uint32_t)trans.ext_stride_1d; + int ext2loc = trans.cmd & MCHAN_CMD_FLAG_DIRECTION_EXT2LOC; + char *loc = (char *)trans.loc; + char *ext = (char *)trans.ext; + if (line == 0) { + return; + } + for (uint32_t moved = 0; moved < total; moved += line) { + uint32_t n = (total - moved < line) ? (total - moved) : line; + if (ext2loc) { + memcpy(loc, ext, n); + } else { + memcpy(ext, loc, n); + } + loc += n; /* local side is contiguous */ + ext += stride; /* external side is strided */ + } +} + +static void mchan_transfer_push(mchan_transfer_t trans) { + if (trans.ext_size_1d < trans.size) { + mchan_transfer_push_2d(trans); + } else { + mchan_transfer_push_1d(trans); + } +} + +static void mchan_transfer_wait(int id) { (void)id; } +static void mchan_transfer_free(int id) { (void)id; } + +#else + static int mchan_transfer_get_id() { return MCHAN_READ_CMD(); } static void mchan_transfer_push_1d(mchan_transfer_t trans) { @@ -137,4 +202,6 @@ static void mchan_transfer_wait(int tid) { #endif } +#endif /* DEEPLOY_L1_AS_L2 */ + #endif