Foundry/tt/tools/emit_tensix_program_header.py

561 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 David (Corey) Herrera
"""
emit_tensix_program_header.py
Turn whole Tensix PROGRAM DIRECTORIES -- the five kernel ELFs plus the five firmware
ELFs that build_tensix_program.sh leaves together -- into foundry/tt/ft_tt_programs.h,
the embedded blob ft_tt_program_blob.h defines.
This is emit_tensix_kernel_header.py one level up. That emitter embeds single kernels
so a Foundry BUILD needs no toolchain; this one embeds whole programs so a Foundry
BINARY needs no program directory. Today Pantheon reads five loose ELFs out of
one absolute programs directory at start-up, which is why the serve runs on exactly one
machine -- and why the rtriad_bfp8 program, built into two per-agent roots and never
into that one, silently cost 1.085x for a day.
Usage:
emit_tensix_program_header.py -o ../ft_tt_programs.h \\
--kernels ../kernels --toolchain 7.68.0 \\
--program ../programs/eltwise \\
--program ../programs/reduce
--program takes a DIRECTORY; the program's name is its basename. Repeat it.
--toolchain and --recipe attach to the --program that follows them, or to all
programs when given before the first one.
------------------------------------------------------------------------------
WHAT IT REFUSES, AND WHY EACH REFUSAL IS HERE
------------------------------------------------------------------------------
A stale artifact has shipped three times in this project (tools/
check_tensix_program_sources.sh names all three). A stale artifact BAKED INTO A BINARY
is worse than any of them, because it survives a rebuild -- so this emitter refuses,
and writes nothing at all, on:
* a kernel image with NO `.prov` provenance record, or one whose recorded input
content no longer matches the tree. This is the CONTENT check
(tools/tensix_image_provenance.py) and it is the one that matters: the `.src`
sidecar's mtime comparison below is defeated by an `explode`, which rewrites every
timestamp, and that is exactly how a reader with no cooperative multicast shipped;
* a `.src` sidecar naming a kernel source that is not in --kernels;
* a `.src` sidecar whose source is NEWER than the ELF beside it;
* a `.src` sidecar with no ELF beside it;
* a program directory with NO sidecars -- built by something that does not record
its sources, so it cannot be checked at all and must not be trusted;
* any of the five fw_*.elf missing -- firmware is all five or none, and a program
whose firmware is incomplete boots a core into whatever L1 held;
* an image that is not a loadable ELF32/LE/ET_EXEC/EM_RISCV Tensix binary, or whose
text is under --min-text-bytes (the silently-empty --just-symbols build);
* two programs with the same name;
* zero programs, unless --allow-empty is passed. An empty blob is a real state -- it
is what the tree carries before the first real regeneration -- but it must be ASKED
for, because the way you get one by accident is a glob that matched nothing.
The sidecar checks are DELEGATED to tools/check_tensix_program_sources.sh rather than
reimplemented: one implementation of that rule, already proven, already the thing the
test Makefiles call. Pass --no-source-check only to test THIS script's own behaviour.
------------------------------------------------------------------------------
DEDUPLICATION
------------------------------------------------------------------------------
Every program directory carries its own copy of the same five firmware images. They are
emitted ONCE per distinct content and shared, so N programs from one build tree cost one
firmware set rather than N. Identical kernel images are shared the same way. The
sharing is by CONTENT, so it can never merge two things that differ.
Status: Written on the Mac. The gates are exercised by tests/Makefile.programblob
against synthetic fixtures. NEVER RUN AGAINST REAL PROGRAM DIRECTORIES -- they
and the SFPI toolchain live on the card host, so a real regeneration is untested.
Issues: None
Todo: None
"""
import hashlib
import io
import os
import shlex
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(os.path.dirname(HERE))
def repo_relative(word):
"""An absolute path inside this checkout, recorded relative to the repo root so the
header's command line carries no machine-specific prefix. Anything else is unchanged."""
if not os.path.isabs(word):
return word
resolved = os.path.realpath(word)
root = os.path.realpath(REPO_ROOT)
if resolved == root or resolved.startswith(root + os.sep):
return os.path.relpath(resolved, root)
return word
sys.path.insert(0, HERE)
# ONE ELF reader, not a second one. emit_tensix_kernel_header.KernelElf already enforces
# every structural rule the launch path needs (ET_EXEC, EM_RISCV, aligned in-bounds
# PT_LOADs, e_entry == first segment) and has been run against real SFPI output.
from emit_tensix_kernel_header import ( # noqa: E402
DEFAULT_MIN_TEXT_BYTES,
PROCESSOR_MACROS,
KernelElf,
emit_blob,
)
from tensix_image_provenance import ( # noqa: E402
ProvenanceError,
check_record,
parse_record,
)
# Indexed by FT_TT_PROC_*: DM0, DM1, MATH0, MATH1, MATH2.
PROCESSOR_ORDER = ["DM0", "DM1", "MATH0", "MATH1", "MATH2"]
KERNEL_FILE_NAMES = ["brisc.elf", "ncrisc.elf", "trisc0.elf", "trisc1.elf", "trisc2.elf"]
FIRMWARE_FILE_NAMES = [
"fw_brisc.elf",
"fw_ncrisc.elf",
"fw_trisc0.elf",
"fw_trisc1.elf",
"fw_trisc2.elf",
]
SOURCE_CHECK_SCRIPT = os.path.join(HERE, "check_tensix_program_sources.sh")
FNV64_OFFSET = 0xCBF29CE484222325
FNV64_PRIME = 0x100000001B3
FNV64_MASK = 0xFFFFFFFFFFFFFFFF
def refuse(message):
raise SystemExit("emit_tensix_program_header: REFUSED -- %s" % message)
def fnv1a_64(data):
"""The digest ft_tt_program_blob.c recomputes. Six lines there, six lines here, on
purpose: the two are meant to be read against each other."""
digest = FNV64_OFFSET
for byte in data:
digest ^= byte
digest = (digest * FNV64_PRIME) & FNV64_MASK
return digest
class ProgramDirectory:
"""One build_tensix_program.sh output directory, validated."""
def __init__(self, path, toolchain, recipe):
self.path = os.path.abspath(path)
self.name = os.path.basename(self.path.rstrip("/"))
self.toolchain = toolchain
self.recipe = recipe
self.set_id = None # the shared firmware set, filled at emit time
self.kernels = [] # (processor_name, file_name, source_name, bytes, KernelElf)
self.firmware = [] # same, all five, in PROCESSOR_ORDER
def load(self, minimum_text, tree_root):
if not os.path.isdir(self.path):
refuse("%s is not a directory" % self.path)
if not self.name or self.name in (".", ".."):
refuse("%s has no usable basename to name a program with" % self.path)
self._load_firmware(minimum_text)
self._load_kernels(minimum_text, tree_root)
if not self.kernels:
refuse("%s holds none of %s -- there is no program here"
% (self.path, ", ".join(KERNEL_FILE_NAMES)))
def _load_firmware(self, minimum_text):
for index, file_name in enumerate(FIRMWARE_FILE_NAMES):
path = os.path.join(self.path, file_name)
if not os.path.isfile(path):
refuse("%s has no %s. Firmware is all five images or none: a program whose "
"firmware set is incomplete boots a core into whatever L1 held."
% (self.path, file_name))
self.firmware.append(self._read_image(path, index, file_name, minimum_text))
def _load_kernels(self, minimum_text, tree_root):
for index, file_name in enumerate(KERNEL_FILE_NAMES):
path = os.path.join(self.path, file_name)
sidecar = path + ".src"
if not os.path.isfile(path):
if os.path.isfile(sidecar):
refuse("%s has %s.src but no %s -- a sidecar with no ELF beside it is a "
"half-written build" % (self.path, file_name, file_name))
continue
text = None
if tree_root is not None:
(text, record) = self._read_provenance(path)
problems = check_record(record, tree_root, self.path)
if problems:
refuse("\n ".join(problems) + "\n Nothing was written.")
self.kernels.append(
self._read_image(path, index, file_name, minimum_text, provenance=text))
def _read_image(self, path, processor_index, file_name, minimum_text, provenance=None):
with open(path, "rb") as handle:
payload = handle.read()
elf = KernelElf(path)
if elf.text_bytes < minimum_text:
refuse("%s text is only %u bytes -- this is the silently-empty --just-symbols "
"build (docs/tt_kernel_toolchain.md 4)" % (path, elf.text_bytes))
return {
"processor": PROCESSOR_ORDER[processor_index],
"file_name": file_name,
"source_name": read_source_name(path),
"payload": payload,
"digest": fnv1a_64(payload),
"elf": elf,
"provenance": provenance,
}
def _read_provenance(self, image_path):
"""The `.prov` record beside a kernel image, verified against the tree it names.
FIRMWARE HAS NONE: the firmware build (tt/fw/) writes no `.prov` record. Only
kernel images reach this."""
sidecar = image_path + ".prov"
if not os.path.isfile(sidecar):
refuse("%s has no provenance record. An image nobody can vouch for is the "
"state that shipped an `rtriad_*` reader with no cooperative multicast "
"for a day -- rebuild it with build_tensix_program.sh, which writes "
"one." % image_path)
with open(sidecar) as handle:
text = handle.read()
try:
record = parse_record(text, sidecar)
except ProvenanceError as error:
refuse(str(error))
return (text, record)
def firmware_set_id(self):
"""A digest of all five firmware images, in order. Two programs whose firmware is
byte-identical get the same id and are emitted once."""
accumulator = hashlib.sha256()
for image in self.firmware:
accumulator.update(image["payload"])
accumulator.update(b"\0")
return accumulator.hexdigest()[:16]
def read_source_name(elf_path):
"""The kernel source an ELF was built from, per its `<elf>.src` sidecar. Firmware has
no sidecar (it is not built from a kernel source), so the file name stands in."""
sidecar = elf_path + ".src"
if os.path.isfile(sidecar):
with open(sidecar) as handle:
name = handle.read().strip()
if name:
return os.path.basename(name)
return os.path.basename(elf_path)
def run_source_check(kernels_directory, program_paths):
"""Delegate the stale-sidecar refusal to the script that already owns it."""
if not os.path.isfile(SOURCE_CHECK_SCRIPT):
refuse("%s is missing -- the stale-source check cannot run, and embedding a program "
"directory nothing checked is exactly the failure this emitter exists to "
"prevent" % SOURCE_CHECK_SCRIPT)
environment = dict(os.environ)
environment["KERNELS"] = kernels_directory
completed = subprocess.run(
["bash", SOURCE_CHECK_SCRIPT] + list(program_paths),
env=environment,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
output = completed.stdout.decode("utf-8", "replace").rstrip()
if output:
print(output)
if completed.returncode != 0:
refuse("at least one program directory is STALE (above). Nothing was written. A "
"stale directory on disk costs a re-run; a stale directory baked into the "
"binary survives a rebuild.")
def parse_arguments(argv):
output_path = None
recorded_output = None
kernels_directory = None
tree_root = None
minimum_text = DEFAULT_MIN_TEXT_BYTES
allow_empty = False
source_check = True
toolchain = "unknown"
recipe = "see the build log beside the directory"
programs = []
index = 0
while index < len(argv):
argument = argv[index]
if argument in ("-o", "--output"):
index += 1
output_path = argv[index]
elif argument == "--recorded-output":
# The header records its own command line, and that line is the only record of
# how the header was made -- so it has to be RUNNABLE. A driver that emits into
# a staging file and installs it afterwards would otherwise record the staging
# path, i.e. a recipe that writes to a temporary file nobody has.
index += 1
recorded_output = argv[index]
elif argument == "--kernels":
index += 1
kernels_directory = argv[index]
elif argument == "--tt-root":
# What the provenance records' input paths are relative to. Defaults to the
# parent of --kernels, which is what the tree looks like everywhere.
index += 1
tree_root = argv[index]
elif argument == "--toolchain":
index += 1
toolchain = argv[index]
elif argument == "--recipe":
index += 1
recipe = argv[index]
elif argument == "--min-text-bytes":
index += 1
minimum_text = int(argv[index])
elif argument == "--allow-empty":
allow_empty = True
elif argument == "--no-source-check":
source_check = False
elif argument == "--program":
index += 1
programs.append(ProgramDirectory(argv[index], toolchain, recipe))
elif argument in ("-h", "--help"):
print(__doc__)
raise SystemExit(0)
else:
raise SystemExit("error: unrecognised argument %r" % argument)
index += 1
if not output_path:
raise SystemExit("error: -o/--output is required")
if not programs and not allow_empty:
refuse("no --program was given. An EMPTY blob is a real state, but it must be asked "
"for with --allow-empty: the way you get one by accident is a glob that "
"matched nothing, and that blob refuses every program at run time.")
if programs and source_check and not kernels_directory:
raise SystemExit("error: --kernels is required (or --no-source-check, which is for "
"testing this script and nothing else)")
names = {}
for program in programs:
if program.name in names:
refuse("two program directories are both named %r (%s and %s). The name is the "
"lookup key, so one would shadow the other."
% (program.name, names[program.name], program.path))
names[program.name] = program.path
if tree_root is None and kernels_directory:
tree_root = os.path.dirname(os.path.abspath(kernels_directory))
return (output_path, recorded_output or output_path, kernels_directory, tree_root,
programs, minimum_text, allow_empty, source_check)
class SymbolPool:
"""Emit each distinct payload once. Keyed by content, so it can never merge two
images that differ."""
def __init__(self):
self.by_digest = {}
self.order = []
def symbol_for(self, payload, hint):
key = hashlib.sha256(payload).hexdigest()
if key not in self.by_digest:
symbol = "ft_tt_program_image_%s_%s" % (hint, key[:12])
self.by_digest[key] = symbol
self.order.append((symbol, payload, hint))
return self.by_digest[key]
def emit_image_row(handle, image, symbol):
handle.write(
' {"%s", %s, %s, sizeof(%s), 0x%016xull,\n'
% (image["source_name"], PROCESSOR_MACROS[image["processor"]], symbol, symbol,
image["digest"])
)
handle.write(emit_provenance(image.get("provenance")))
handle.write(" },\n")
def emit_provenance(text):
"""The `.prov` record as C string literals, ONE PER LINE.
Line per line rather than one escaped blob, because this is the half of the header a
human reads: a diff of a regenerated blob then shows which input moved, in words,
instead of a single changed 900-character string."""
if text is None:
# Firmware. NULL is the honest value -- see ProgramDirectory._read_provenance.
return " NULL\n"
lines = [' "%s\\n"\n' % line.replace("\\", "\\\\").replace('"', '\\"')
for line in text.splitlines()]
return "".join(lines)
def emit_header(handle, programs, command_line, allow_empty):
pool = SymbolPool()
firmware_sets = {}
for program in programs:
set_id = program.firmware_set_id()
if set_id not in firmware_sets:
firmware_sets[set_id] = program
program.set_id = set_id
handle.write(HEADER_PREAMBLE % command_line)
for image in _every_payload(programs):
pool.symbol_for(image["payload"], image["hint"])
for (symbol, payload, _hint) in pool.order:
handle.write("/* %u bytes */\n" % len(payload))
emit_blob(handle, symbol, payload)
for (set_id, program) in sorted(firmware_sets.items()):
handle.write(
"/* firmware set %s -- first seen in %s */\n"
"static const ft_tt_program_blob_firmware ft_tt_program_firmware_%s = {\n"
' "%s",\n {\n' % (set_id, program.name, set_id, set_id)
)
for image in program.firmware:
handle.write(" ")
emit_image_row(handle, image, pool.symbol_for(image["payload"], "fw"))
handle.write(" }\n};\n\n")
for program in programs:
handle.write(
"static const ft_tt_program_blob_image ft_tt_program_%s_kernels[] = {\n"
% program.name
)
for image in program.kernels:
emit_image_row(handle, image, pool.symbol_for(image["payload"], "k"))
handle.write("};\n\n")
handle.write(
"static const ft_tt_program_blob_entry ft_tt_program_%s = {\n"
' "%s", "%s", "%s",\n'
" &ft_tt_program_firmware_%s,\n"
" ft_tt_program_%s_kernels,\n"
" (unsigned)(sizeof(ft_tt_program_%s_kernels) / "
"sizeof(ft_tt_program_%s_kernels[0]))\n};\n\n"
% (program.name, program.name, program.toolchain, program.recipe,
program.set_id, program.name, program.name, program.name)
)
if programs:
handle.write("static const ft_tt_program_blob_entry *const "
"ft_tt_program_blob_table_storage[] = {\n")
for program in programs:
handle.write(" &ft_tt_program_%s,\n" % program.name)
handle.write("};\n\n")
handle.write("static const ft_tt_program_blob_entry *const *const "
"ft_tt_program_blob_table = ft_tt_program_blob_table_storage;\n")
handle.write("#define FT_TT_PROGRAM_BLOB_TABLE_COUNT "
"((unsigned)(sizeof(ft_tt_program_blob_table_storage) / "
"sizeof(ft_tt_program_blob_table_storage[0])))\n\n")
else:
if not allow_empty:
refuse("internal: reached the empty emission without --allow-empty")
handle.write(EMPTY_TABLE)
handle.write("#endif /* FT_TT_PROGRAMS_H */\n")
def _every_payload(programs):
"""Firmware first so the shared sets are emitted before the per-program kernels."""
for program in programs:
for image in program.firmware:
yield dict(image, hint="fw")
for program in programs:
for image in program.kernels:
yield dict(image, hint="k")
HEADER_PREAMBLE = '''/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_programs.h
* @brief GENERATED -- do not edit. Embedded Tensix PROGRAM directories for the Foundry
* Blackhole backend, in the format ft_tt_program_blob.h defines.
*
* Produced by foundry/tt/tools/emit_tensix_program_header.py from directories built with
* foundry/tt/build_tensix_program.sh. Regenerate rather than editing:
* make -f tools/Makefile regen-programs (on the card host -- SFPI lives there)
*
* Each program carries BOTH its five kernel ELFs and the five fw_*.elf they were linked
* against, because a kernel's text sits at its own firmware's __fw_export_text_end and the
* two are one build output. Identical images are emitted once and shared.
*
* Command line:
* %s
*
* @status Generated artifact.
* @issues None
* @todo None
*/
#ifndef FT_TT_PROGRAMS_H
#define FT_TT_PROGRAMS_H
#include "ft_tt_memmap.h"
#include "ft_tt_program_blob.h"
'''
EMPTY_TABLE = '''/* NO PROGRAMS. This blob was generated with --allow-empty, which is what the tree carries
* until the first real regeneration on the card host. Every lookup refuses and says so; nothing
* silently falls back. */
static const ft_tt_program_blob_entry *const *const ft_tt_program_blob_table = NULL;
#define FT_TT_PROGRAM_BLOB_TABLE_COUNT 0u
'''
def main(argv):
(output_path, recorded_output, kernels_directory, tree_root, programs, minimum_text,
allow_empty, source_check) = parse_arguments(argv)
if programs and source_check:
run_source_check(kernels_directory, [program.path for program in programs])
else:
# --no-source-check is the escape hatch for testing THIS script, and it turns off
# the provenance check with it: both checks need the source tree, and half a check
# is the state that reads as coverage.
tree_root = None
for program in programs:
program.load(minimum_text, tree_root)
recorded_argv = []
skip_next = False
for (position, word) in enumerate(argv):
if skip_next:
skip_next = False
continue
if word == "--recorded-output":
skip_next = True
continue
if position >= 1 and argv[position - 1] in ("-o", "--output"):
recorded_argv.append(repo_relative(recorded_output))
continue
recorded_argv.append(repo_relative(word))
command_line = " ".join(
shlex.quote(word) for word in [os.path.basename(sys.argv[0])] + recorded_argv
)
# EMITTED IN FULL BEFORE THE OUTPUT IS TOUCHED. `open(path, "w")` truncates on the way
# in, so emitting straight into the file would let a refusal half-way through destroy
# the committed header -- turning a refusal into damage.
buffer = io.StringIO()
emit_header(buffer, programs, command_line, allow_empty)
with open(output_path, "w") as handle:
handle.write(buffer.getvalue())
kernel_bytes = sum(len(i["payload"]) for p in programs for i in p.kernels)
firmware_bytes = sum(len(i["payload"]) for p in programs for i in p.firmware)
distinct = len({hashlib.sha256(i["payload"]).hexdigest()
for p in programs for i in (p.kernels + p.firmware)})
print("wrote %s: %u program(s), %u distinct image(s), %u B kernels + %u B firmware "
"before dedup" % (output_path, len(programs), distinct, kernel_bytes,
firmware_bytes))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))