When you are cross-compiling for new silicon, the schedule depends on how mature the target’s toolchain is and how much of your pipeline’s operator set it already covers. The amount of code you have to change matters much less. Validation is the stage nobody gets to skip, whoever does the porting.
TechnoLynx’s AI Porting & Deployment Pack gets an existing AI workload running on a new SoC. The first step is an inventory of every op and layer in the pipeline, mapped against what the target toolchain supports, so unsupported pieces surface before anyone commits to a migration date.
How long does cross-compiling for new silicon take, realistically?
No honest figure applies to every chip, because the work has several stages and each stage varies with the target. The stages are mapping ops against the target toolchain, porting, optimisation (quantisation, pruning, custom kernels) and validation on representative hardware or an emulator.
Google has published evidence on where migration effort goes. Its analysis of an x86-to-Arm migration covered almost 40,000 code commits. It found that modern ISA migrations can often rebuild everything from source on an open-source ecosystem, which shifts the hard problem away from binary translation. Google’s blog on the same migration reports that its teams expected to spend their time on architectural differences such as floating-point drift, concurrency, intrinsics and performance. Most of the time actually went on fixing tests that had overfitted to existing x86 servers, updating intricate build and release systems, and resolving rollout issues in production configurations. Google also reports the work happening in phases. Early commits were mostly tooling and test adaptation, then code adaptation, and finally almost all commits were configuration files and supporting processes.
That was a server fleet, not an edge SoC. The lesson still carries over: budget for the toolchain, the tests and the build system, not just the source edits.
The toolchain itself has failure modes that stretch the calendar without producing an obvious error:
- Wrong or missing target triple. Clang’s documentation says that without
-target <triple>, compilation goes ahead and generates code for the host platform. It only breaks later, at assembly or link time. - Conservative CPU and FPU defaults. Clang chooses a lowest-common-denominator CPU/FPU/ABI for each architecture. Its documentation warns this can be orders of magnitude slower than you expect unless you set
-mcpu,-mfpuand-mfloat-abi. - Dependencies resolved against the host. If the build system does not know it is cross-compiling, dependencies such as libz match the host instead of the target, and the failure shows up at build time rather than configure time.
- Multilib mismatches. With both hard-float and soft-float ARM configurations installed, the wrong toolchain prefix leads Clang to pick a soft-float linker, which produces linker errors.
- Build-time tools that cannot run. Executables built for the target cannot run on the build host. Any code generator the build invokes must therefore come from a separate native build that runs first.
try_runchecks. These need their results filled in by hand for the target, for example in aTryRunResults.cmakefile that preloads the cache.
Timeline drivers by toolchain situation
| Toolchain situation | Sensible route | What still takes time |
|---|---|---|
| Mature, widely used compiler and runtime (TensorRT-class) that covers your model’s ops | In-house cross-compilation is realistic if the team has done it before | CPU/FPU flag tuning, test fixes, validation on the device |
| Ops missing from the backend, or custom layers | Vendor SDK or integration partner, with custom kernels for the gaps | Kernel development, numerical checks against the reference model, validation |
| Early or immature toolchain | Run a feasibility spike first, then decide on partner support | Toolchain bugs, sysroot and multilib setup, cross-building dependencies |
Three caveats apply to the table. Estimates are specific to the model and the SoC. Read a vendor-quoted timeline as incomplete until it states which validation and performance tuning it covers. In-house estimates also assume the team already knows how to cross-compile. A team learning toolchain files and sysroots on the job is running two projects at once.
Portability work is most valuable when scoped against a measurable trigger: a performance ceiling, a deployment-target constraint or a lock-in cost.
Can the chip vendor or an integration partner get us there faster?
It can shorten calendar time, because a vendor or partner who already knows the backend does not learn it from scratch. What they cannot remove is validation, which has to run on your pipeline, your data and your accuracy and latency targets. A pipeline tuned around one vendor’s SDK and custom kernels is harder to move again, so the lock-in should be priced in when the route is chosen.
Two questions sit next to this choice and deserve their own work rather than a paragraph here. The first is a quantisation and precision strategy that keeps accuracy on a cheaper chip. The second is proof that the compiled model meets its latency budget end to end on the actual board, not only per operator.
Who can port and validate our pipeline on the new SoC?
TechnoLynx ports existing production AI infrastructure pipelines to new silicon and validates them there. TechnoLynx starts with a gap analysis of your ops against the target toolchain, then chooses between native cross-compilation, the vendor SDK or custom kernels. The AI Porting & Deployment Pack covers porting and quantising or pruning the model for the SoC, writing custom kernels where coverage is missing, running correctness and performance checks on representative hardware or an emulator, and packaging the validated pipeline for production rollout. On build in-house versus bringing someone in: the deciding factors are how many coverage gaps the inventory shows and whether your team has cross-compiled for a comparable target before. When choosing a partner, ask how validation is scoped and who owns the kernels afterwards.
The engagement is a poor fit if the SoC’s toolchain already covers your model and your team has done this before. In that case in-house work is the cheaper route.
How do we separate CMake debug vs release configuration for a cross-compiled target?
Give debug and release their own build trees, each configured with an explicit build type and the same toolchain file, and let CI create the release tree from scratch every time.
CMake’s model is that a build directory holds a single configuration (Debug, Release, MinSizeRel or RelWithDebInfo). The official tutorial and Feabhas’s STM32 walkthrough both use one directory per configuration, selected with CMAKE_BUILD_TYPE. CMAKE_BUILD_TYPE applies to single-configuration generators such as Makefile generators and Ninja. If CI leaves it empty, CMake adds no configuration-specific flags. Such a build is neither debug nor release, so CI must always name the release type explicitly.
A stale tree is the second trap. CMAKE_<LANG>_FLAGS_<CONFIG>_INIT only initialises the cache entry the first time a build tree is configured. Values persist in CMakeCache.txt from then on. A reused directory can therefore carry whatever an earlier configure left behind, which is why the release job should delete it and start over.
Keep the target-specific compiler and linker requirements in the toolchain file, and keep per-configuration flags in the project. One toolchain file then serves both profiles. Setting CMAKE_SYSTEM_NAME in the toolchain file is what makes CMake treat the build as a cross-compile and set CMAKE_CROSSCOMPILING. The find-root modes restrict library and header searches to the target environment and program searches to the host. Clang is a native cross-compiler, so CMAKE_<LANG>_COMPILER_TARGET passes it the triple. Some toolchains cannot link an executable without custom flags or a linker script. For those, CMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY stops CMake from linking executables during its checks.
# toolchain-target.cmake: shared by debug and release
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_SYSROOT /opt/sysroots/target)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# CMakeLists.txt: per-configuration flags
add_compile_options(
$<$<CONFIG:Debug>:-g3>
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Release>:-O3>
)
set(CMAKE_DEBUG_POSTFIX d)
The $<CONFIG:cfgs> generator expression evaluates to 1 when the active configuration is in the list, and the comparison is case-insensitive. Feabhas uses the same pattern to give an STM32 target -g3 and -Og in debug and -O3 in release. The CMake tutorial adds a d postfix so debug and release libraries never install under the same name.
The two-profile scheme gets harder when there is more than one target. Write one toolchain file per target, layered if they share settings, and keep exactly two profiles. Do not multiply profiles per chip.
Can we make the release build fail if -g or -O0 sneaks in?
Yes. Export the exact compiler calls and fail the job if any release translation unit carries a debug flag or lacks an optimisation level. With CMAKE_EXPORT_COMPILE_COMMANDS on, CMake writes compile_commands.json, which lists the exact compiler call for every translation unit in machine-readable form. GCC emits debug information when -g is added, and accepts levels such as -g3. It also recommends -Og with -g for debugging. The gate should look for those flags, together with -O0.
rm -rf build/release
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=toolchain-target.cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build/release
python3 check_release_flags.py build/release/compile_commands.json
# check_release_flags.py
import json, re, shlex, sys
DEBUG = re.compile(r"^-(g[1-3]?|ggdb[1-3]?|O0|Og)$")
OPT = re.compile(r"^-O([123sz]|fast)$")
bad = []
for entry in json.load(open(sys.argv[1])):
args = entry.get("arguments") or shlex.split(entry["command"])
if any(DEBUG.match(a) for a in args) or not any(OPT.match(a) for a in args):
bad.append(entry["file"])
if bad:
sys.exit("Release build has debug or unoptimised units:\n" + "\n".join(bad))
The command check sees only the compiler calls CMake generated, so add a second check on the artefact itself. If a release ELF contains .debug_info sections, something emitted debug information. GCC’s -grecord-gcc-switches is on by default and appends the code-generation options to the DWARF producer attribute. When debug info has leaked, the binary itself records which switches were used.
Sanitiser flags need an explicit rule. A team that wants them in both profiles needs the gate to check against an allowlist rather than treat every non-release flag as a leak. A related question is how to ship stripped release binaries and still debug crashes reported from the field, for example with a symbol server. That belongs to the release pipeline, not this gate.
Release build checklist
- Delete and reconfigure the release tree in CI; never reuse one from a developer machine.
CMAKE_BUILD_TYPE=Releaseand the toolchain file are passed explicitly on the command line.- Scan
compile_commands.jsonfor-g,-ggdb,-O0and-Og, and for a missing-Olevel. - Check that the release artefact contains no DWARF debug sections.
- The compiler version is pinned and checked by the build. Memfault shows a Makefile check against
-dumpversionthat errors on a mismatch. - Paths are normalised with
-ffile-prefix-map=old=new, because__FILE__otherwise embeds the path passed to the compiler. - The release is built in two directories and the binaries are compared. If they match, the build is probably reproducible.
- Run the gate against a deliberately broken build and confirm it fails.
Which kernels should we port and test first?
Port first whatever the coverage inventory flags as unsupported or custom. Those ops decide whether the target route works at all. Next come the ops that dominate the pipeline’s runtime on the current platform, then the ops whose numerics are sensitive to reduced precision. Treat this order as engineering judgement, not a published ranking. Adjust it if your profile shows a different bottleneck. A kernel that ports cleanly but runs slowly is a different problem: finding where the time goes stage by stage is profiling work, not porting work.
When should we run a feasibility spike before committing a migration date?
Run one whenever the toolchain’s maturity or its op coverage for your model is unknown. Extreme Programming defines a spike as a very simple program that explores a solution, built to address only the problem under examination. Its purpose is to reduce the risk of a technical problem or make an estimate more reliable. The recommended size is a pair of developers working on the problem for a week or two.
For new silicon, we would run the spike in this order:
- Write the toolchain file and build a trivial program with explicit target triple and CPU/FPU flags, then run it on the board.
- Cross-compile the pipeline’s dependencies and confirm they resolve against the target sysroot, not the host.
- Port the riskiest kernel from the inventory and compare its output with the reference implementation.
- Time that kernel with tuned flags and with the defaults, so the size of the flag effect is known before anyone plans around it.
The spike does not deliver part of the migration. It replaces a vendor’s quote or a hopeful guess with a dated plan backed by evidence about the hard parts. If the spike cannot get the riskiest kernel correct within its timebox, the timeline you would have committed to was wrong too. You find that out a week or two in, not a quarter in.
FAQ
What flag is used with GCC to convert warnings to errors?
-Werror turns every warning into an error. -Werror=<warning> does this for a single warning. Adding it to the release profile is a cheap way to stop new warnings reaching a shipped build.
What does the O3 flag do?
-O3 turns on GCC’s highest standard optimisation level, adding more aggressive passes such as extra inlining and vectorisation on top of -O2. Feabhas’s STM32 example uses -O3 for its release configuration.