Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions Deeploy/Targets/GAP9/DMA/MchanDma.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
19 changes: 16 additions & 3 deletions Deeploy/Targets/GAP9/Templates/AllocateTemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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("")

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions DeeployTest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions DeeployTest/Platforms/GAP9/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions TargetLibraries/GAP9/inc/mchan.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <string.h>

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) {
Expand Down Expand Up @@ -137,4 +202,6 @@ static void mchan_transfer_wait(int tid) {
#endif
}

#endif /* DEEPLOY_L1_AS_L2 */

#endif
Loading