220 lines
11 KiB
Bash
Executable File
220 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
# Copyright (C) 2026 David (Corey) Herrera
|
|
#
|
|
# build_tensix_dataflow_kernel.sh
|
|
# Compiles ONE data-movement kernel -- BRISC (NoC 0) or NCRISC (NoC 1) -- into an ELF,
|
|
# together with the processor's firmware ELF whose exported symbols that link needs, from
|
|
# Foundry sources only (tt/fw/, tt/kernels/) with the SFPI compiler. build_tensix_kernel.sh
|
|
# does the compute (TRISC0/1/2) part of the triad. Both take every architecture- and
|
|
# board-shaped value from tensix_kernel_build_arch.sh.
|
|
#
|
|
# Usage: build_tensix_dataflow_kernel.sh <kernel.c> <brisc|ncrisc> [outdir]
|
|
# Env: TT_ARCH / TT_NUM_L1_BANKS / TT_NUM_DRAM_BANKS
|
|
# as build_tensix_kernel.sh; see tensix_kernel_build_arch.sh
|
|
# CTARGS comma-separated KERNEL_COMPILE_TIME_ARGS (default 0)
|
|
# NOC NoC index this kernel issues on (default 0 for brisc, 1 for ncrisc)
|
|
# KERNEL_DEFINES / LIGHTWEIGHT_ASSERTS as build_tensix_kernel.sh
|
|
# KERNEL_TEXT_BASE link this kernel's text at an explicit L1 address instead of at
|
|
# __fw_export_text_end (build_tensix_kernel.sh's KERNEL TEXT RELOCATION
|
|
# block). NCRISC's firmware tail is small
|
|
# Output: <outdir>/<proc>.elf and <outdir>/fw_<proc>.elf, with .src / .prov records.
|
|
#
|
|
# Differences from the compute builder: -mcpu tt-bh (no Tensix co-processor
|
|
# instructions); -Os; -fno-tree-loop-distribute-patterns (no loop becomes a library
|
|
# call); COMPILE_FOR_BRISC / COMPILE_FOR_NCRISC and NOC_INDEX instead of the TRISC
|
|
# defines; the entry is fw/ft_dataflow_entry.c; BRISC firmware also links fw/ft_noc.c.
|
|
#
|
|
# NOC_INDEX is baked in at compile time, but the firmware takes its NoC at run time from
|
|
# the launch message's brisc_noc_id (fw/ft_brisc_fw.c). Host and kernel must agree.
|
|
#
|
|
# The `objcopy --weaken` step below is load-bearing: point --just-symbols at the
|
|
# un-weakened firmware and LTO drops our _start, giving a clean link and an empty
|
|
# kernel. Full write-up: docs/tt_kernel_toolchain.md 4.
|
|
#
|
|
# Status: verified on the p150a card host 2026-08-11 and since; staged-tree paths removed
|
|
# 2026-10-08 (clean-room Phase 7) with every image byte-identical.
|
|
# Issues: None
|
|
# Todo: None
|
|
#
|
|
set -euo pipefail
|
|
SFPI=${SFPI:-/opt/tenstorrent/sfpi}
|
|
GCC=$SFPI/compiler/bin/riscv-tt-elf-gcc
|
|
NM=$SFPI/compiler/bin/riscv-tt-elf-nm
|
|
SIZE=$SFPI/compiler/bin/riscv-tt-elf-size
|
|
OBJCOPY=$SFPI/compiler/bin/riscv-tt-elf-objcopy
|
|
|
|
KERNEL=${1:?usage: build_tensix_dataflow_kernel.sh <kernel.c> <brisc|ncrisc> [outdir]}
|
|
PROC=${2:?usage: build_tensix_dataflow_kernel.sh <kernel.c> <brisc|ncrisc> [outdir]}
|
|
OUT=${3:-/tmp/ttk}
|
|
KERNEL=$(cd "$(dirname "$KERNEL")" && pwd)/$(basename "$KERNEL")
|
|
|
|
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
# shellcheck source=tensix_kernel_build_arch.sh
|
|
. "$HERE/tensix_kernel_build_arch.sh"
|
|
|
|
case $PROC in
|
|
brisc) PROC_ID=0; PROC_DEFINE=COMPILE_FOR_BRISC; DEFAULT_NOC=0 ;;
|
|
ncrisc) PROC_ID=1; PROC_DEFINE=COMPILE_FOR_NCRISC; DEFAULT_NOC=1 ;;
|
|
*) echo "error: processor must be brisc or ncrisc, got '$PROC'" >&2; exit 2 ;;
|
|
esac
|
|
NOC=${NOC:-$DEFAULT_NOC}
|
|
|
|
GEN=$OUT/gen_$PROC
|
|
mkdir -p "$GEN"
|
|
|
|
case $KERNEL in
|
|
*.c) KERNEL_LANGUAGE=c ;;
|
|
*.cpp) echo "error: C++ kernels are not supported -- the kernel entry is Foundry's C" \
|
|
"(fw/ft_*_entry.c, clean-room plan Phase 2); port '$KERNEL' to C" >&2; exit 2 ;;
|
|
*) echo "error: kernel must be .c, got '$KERNEL'" >&2; exit 2 ;;
|
|
esac
|
|
|
|
# ---- Include set, flags and defines ------------------------------------------------
|
|
# Kernels include Foundry's headers by relative path; $GEN holds this build's own outputs.
|
|
INC="-I$GEN"
|
|
|
|
# -Os keeps the NoC helpers out of line (O3 inlines them into a much larger .text).
|
|
# C11 for _Static_assert (the CB-interface stride pin in ft_tensix_circular_buffer.h).
|
|
KERNEL_CFLAGS="-mcpu=$TT_MCPU_DM -fno-tree-loop-distribute-patterns -std=c11 -flto=auto \
|
|
-ffast-math -Os -Wall -Wextra -Wno-unused-variable -Wno-unused-function"
|
|
|
|
# LIGHTWEIGHT_KERNEL_ASSERTS: as build_tensix_kernel.sh (on by default; the entry's
|
|
# post-kernel NoC checks and the CB overrun guard compile to `ebreak`). NOC_MODE=0 is the
|
|
# dedicated-NoC mode, the only one Foundry's firmware serves.
|
|
ASSERT_DEFINE=""
|
|
if [ "${LIGHTWEIGHT_ASSERTS:-1}" = 1 ]; then
|
|
ASSERT_DEFINE="-DLIGHTWEIGHT_KERNEL_ASSERTS"
|
|
fi
|
|
|
|
DEF="-DTENSIX_FIRMWARE -DLOCAL_MEM_EN=0 $ASSERT_DEFINE $TT_ARCH_DEFINE -D$PROC_DEFINE \
|
|
-DPROCESSOR_INDEX=$PROC_ID -DPROGRAMMABLE_CORE_TYPE=0 -DDISPATCH_MESSAGE_ADDR=0 \
|
|
-DNOC_INDEX=$NOC -DNOC_MODE=0 \
|
|
$TT_BOARD_DEFINES"
|
|
|
|
# Foundry's firmware has no watcher (build_tensix_kernel.sh's WATCHER note).
|
|
if [ "${WATCHER:-0}" != 0 ]; then
|
|
echo "error: WATCHER=${WATCHER} -- Foundry's firmware has no watcher (clean-room Phase 7)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# The data-movement half of build_tensix_kernel.sh's KERNEL_DEFINES, and it has to exist
|
|
# here too: the matmul READER reads the same resident_matmul_block_protocol.h the compute
|
|
# thread does, so a section size the two disagree about is a reader that refuses every
|
|
# descriptor the compute thread would have run. Omitted from the recipe when empty.
|
|
KERNEL_DEFINES=${KERNEL_DEFINES:-}
|
|
if [ -n "$KERNEL_DEFINES" ]; then
|
|
DEF="$DEF $KERNEL_DEFINES"
|
|
echo "### KERNEL_DEFINES: $KERNEL_DEFINES"
|
|
fi
|
|
|
|
LDFLAGS="-flto=auto -fno-exceptions -fno-rtti -nostdlib -Wl,-z,max-page-size=16 \
|
|
-Wl,-z,common-page-size=16 -Wno-stringop-overflow"
|
|
|
|
|
|
# ---- Support objects (fw/ft_libc.c, fw/ft_crt0.S), into $GEN so a shared output
|
|
# directory never clobbers the compute path's own copies.
|
|
echo "### support objects for $PROC"
|
|
rm -f "$GEN/ft_libc.o" "$GEN/ft_crt0.o"
|
|
$GCC $KERNEL_CFLAGS -c "$HERE/fw/ft_libc.c" -o "$GEN/ft_libc.o" ||
|
|
{ echo "### ABORT: fw/ft_libc.c did not compile"; exit 1; }
|
|
$GCC -mcpu=$TT_MCPU_DM -c "$HERE/fw/ft_crt0.S" -o "$GEN/ft_crt0.o" ||
|
|
{ echo "### ABORT: fw/ft_crt0.S did not assemble"; exit 1; }
|
|
# The runtime ARCHIVE: our memcpy (fw/ft_libc.c -DFT_LIBC_RUNTIME) and binary32 helpers
|
|
# (fw/ft_softfloat.c), the only runtime calls GCC can emit here. Every link is -nostdlib, so no
|
|
# toolchain library reaches an image; built WITHOUT LTO and linked as an archive so a member
|
|
# joins only an image that calls it and every other image is unchanged.
|
|
rm -f "$GEN/ft_rt_libc.o" "$GEN/ft_softfloat.o" "$GEN/libft_rt.a"
|
|
AR=$SFPI/compiler/bin/riscv-tt-elf-ar
|
|
RT_CFLAGS="-mcpu=$TT_MCPU_DM -std=c11 -Os -Wall -Wextra -ffreestanding \
|
|
-fno-tree-loop-distribute-patterns"
|
|
# RUNTIME_DEFINES: red builds only (e.g. -DFT_SOFTFLOAT_RED_COMPARE_INVERTED); never shipped.
|
|
RT_CFLAGS="$RT_CFLAGS ${RUNTIME_DEFINES:-}"
|
|
[ -n "${RUNTIME_DEFINES:-}" ] && echo "### RUNTIME_DEFINES (RED BUILD): $RUNTIME_DEFINES"
|
|
$GCC $RT_CFLAGS -DFT_LIBC_RUNTIME -c "$HERE/fw/ft_libc.c" -o "$GEN/ft_rt_libc.o" &&
|
|
$GCC $RT_CFLAGS -c "$HERE/fw/ft_softfloat.c" -o "$GEN/ft_softfloat.o" &&
|
|
$AR rcs "$GEN/libft_rt.a" "$GEN/ft_rt_libc.o" "$GEN/ft_softfloat.o" ||
|
|
{ echo "### ABORT: runtime archive did not build"; exit 1; }
|
|
LINK_OBJS="$GEN/ft_libc.o $GEN/libft_rt.a"
|
|
FW_OBJS=""
|
|
if [ "$PROC" = brisc ]; then
|
|
# BRISC firmware's NoC setup (fw/ft_noc.c, clean-room Phase 6). Firmware only: no kernel
|
|
# calls into it.
|
|
rm -f "$GEN/ft_noc.o"
|
|
$GCC $KERNEL_CFLAGS $DEF -DFW_BUILD -c "$HERE/fw/ft_noc.c" -o "$GEN/ft_noc.o" ||
|
|
{ echo "### ABORT: fw/ft_noc.c did not compile"; exit 1; }
|
|
FW_OBJS="$GEN/ft_noc.o"
|
|
fi
|
|
|
|
# ---- 4. Firmware ELF: the kernel link needs its __fw_export_* symbols ------------------
|
|
echo "### firmware $PROC"
|
|
rm -f "$GEN/fw_$PROC.o"
|
|
# Foundry's firmware: fw/ft_brisc_fw.c (clean-room Phase 6) or fw/ft_ncrisc_fw.c (Phase 5).
|
|
$GCC $KERNEL_CFLAGS $DEF -DFW_BUILD -I"$HERE" -I"$HERE/kernels" -c \
|
|
"$HERE/fw/ft_${PROC}_fw.c" -o "$GEN/fw_$PROC.o"
|
|
[ -f "$GEN/fw_$PROC.o" ] || { echo "### ABORT: firmware $PROC did not compile"; exit 1; }
|
|
python3 "$HERE/tools/emit_tensix_linker_script.py" --cc "$GCC" --memmap "$HERE/ft_tt_memmap.h" \
|
|
--processor $PROC --type firmware --template "$HERE/fw/ft_tensix_firmware.ld" \
|
|
-o "$GEN/firmware_$PROC.ld" || { echo "### ABORT: firmware_$PROC.ld"; exit 1; }
|
|
$GCC -mcpu=$TT_MCPU_DM $LDFLAGS -Os -T"$GEN/firmware_$PROC.ld" \
|
|
"$GEN/ft_crt0.o" "$GEN/fw_$PROC.o" $FW_OBJS $LINK_OBJS -o "$OUT/fw_$PROC.elf"
|
|
$SIZE "$OUT/fw_$PROC.elf"
|
|
|
|
# ---- 5. Kernel ELF, linked against a WEAKENED copy of that firmware --------------------
|
|
echo "### kernel $PROC (NOC_INDEX=$NOC, CTARGS=${CTARGS:-0})"
|
|
# -MMD: the depfile is what tools/record_tensix_image_provenance.py records this image's
|
|
# input closure from. It is the COMPILER'S OWN list, not a scan of #include lines, which is
|
|
# the difference between a closure that tracks the build and one that drifts from it.
|
|
# The entry is Foundry's (fw/ft_dataflow_entry.c), one source for both processors.
|
|
rm -f "$GEN/$PROC.o"
|
|
$GCC $KERNEL_CFLAGS $DEF -DKERNEL_BUILD -I"$HERE" -MMD -MF "$GEN/$PROC.d" -c \
|
|
"$HERE/fw/ft_dataflow_entry.c" -o "$GEN/$PROC.o" ||
|
|
{ echo "### ABORT: fw/ft_dataflow_entry.c did not compile for $PROC"; exit 1; }
|
|
KERNEL_OBJS=""
|
|
DEPFILES=(--depfile "$GEN/$PROC.d")
|
|
if [ "$KERNEL_LANGUAGE" = c ]; then
|
|
# The kernel is its own object, compiled with this processor's $DEF: NOC_INDEX /
|
|
# NUM_NOCS / COMPILE_FOR_* are what select its register block.
|
|
$GCC $KERNEL_CFLAGS $DEF -DKERNEL_BUILD -DKERNEL_COMPILE_TIME_ARGS="${CTARGS:-0}" $INC \
|
|
-MMD -MF "$GEN/kernel_body.d" -c "$KERNEL" -o "$GEN/kernel_body.o"
|
|
KERNEL_OBJS="$GEN/kernel_body.o"
|
|
DEPFILES+=(--depfile "$GEN/kernel_body.d")
|
|
fi
|
|
python3 "$HERE/tools/emit_tensix_linker_script.py" --cc "$GCC" --memmap "$HERE/ft_tt_memmap.h" \
|
|
--processor $PROC --type kernel --template "$HERE/fw/ft_tensix_kernel.ld" \
|
|
-o "$GEN/kernel_$PROC.ld" || { echo "### ABORT: kernel_$PROC.ld"; exit 1; }
|
|
echo " __fw_export_text_end=0x$($NM "$OUT/fw_$PROC.elf" | awk '/__fw_export_text_end/{print $1}')" \
|
|
" __fw_export_ldm_end=0x$($NM "$OUT/fw_$PROC.elf" | awk '/__fw_export_ldm_end/{print $1}')"
|
|
$OBJCOPY --weaken "$OUT/fw_$PROC.elf" "$GEN/fw_${PROC}_weak.elf"
|
|
TEXT_DEFSYM=""
|
|
if [ -n "${KERNEL_TEXT_BASE:-}" ]; then
|
|
TEXT_DEFSYM="-Wl,--defsym=__fw_export_text_end=$KERNEL_TEXT_BASE"
|
|
echo " relocating kernel text to $KERNEL_TEXT_BASE"
|
|
fi
|
|
$GCC -mcpu=$TT_MCPU_DM $LDFLAGS -Os -T"$GEN/kernel_$PROC.ld" $TEXT_DEFSYM \
|
|
-Wl,--just-symbols="$GEN/fw_${PROC}_weak.elf" \
|
|
"$GEN/$PROC.o" $KERNEL_OBJS $LINK_OBJS -o "$OUT/$PROC.elf"
|
|
$SIZE "$OUT/$PROC.elf"
|
|
|
|
# The ELF is named after the processor, so record which kernel it came from for
|
|
# tools/emit_tensix_kernel_header.py to put in the generated header's source_name.
|
|
basename "$KERNEL" > "$OUT/$PROC.elf.src"
|
|
|
|
# ...and record WHAT IT WAS BUILT FROM, by content. The `.src` sidecar names one file and
|
|
# is checked by mtime, which an `explode` resets; the `.prov` record hashes every in-tree
|
|
# file the compile opened plus the recipe that reached code generation, and nothing about
|
|
# timestamps can launder that. tools/tensix_image_provenance.py has the incident it is
|
|
# written against.
|
|
RECIPE_DEFINES=()
|
|
[ -n "$KERNEL_DEFINES" ] && RECIPE_DEFINES=(--recipe "defines=$KERNEL_DEFINES")
|
|
python3 "$HERE/tools/record_tensix_image_provenance.py" \
|
|
--tt-root "$HERE" --image "$OUT/$PROC.elf" --source "$KERNEL" "${DEPFILES[@]}" \
|
|
--recipe "toolchain=$($GCC -dumpversion)" \
|
|
--recipe "ctargs=${CTARGS:-0}" \
|
|
--recipe "noc=$NOC" \
|
|
--recipe "watcher=${WATCHER:-0}" \
|
|
--recipe "lightweight_asserts=${LIGHTWEIGHT_ASSERTS:-1}" \
|
|
--recipe "relocate_kernel_text=${RELOCATE_KERNEL_TEXT:-0}" \
|
|
--recipe "kernel_text_base=${KERNEL_TEXT_BASE:-}" "${RECIPE_DEFINES[@]}"
|
|
echo "__DATAFLOW_DONE__ $PROC"
|