The packaging commit shipped the code and a summary of the numbers. It left out
everything the numbers are measured *against*, which is most of what makes them
checkable.
Added:
baselines/ llama.cpp runs (thread sweeps, NUMA sweeps, the
interleaved A/B passes behind the headline decode
figure) and the quality references: each build
against its own bf16 over 12 greedy prompts.
benchmarks/.../results/ 102 artifacts backing the published figures, each
with a provenance block naming the model,
quantization, thread set and build SHAs.
docs/engines/ the engine analyses -- llama.cpp, ExecuTorch, MNN,
ncnn, vLLM, vLLM-Omni -- and the cross-engine and
device-simulation comparisons.
docs/analysis/ the full study documents the summaries distil.
docs/edge/comparisons.md where this fork sits against llama.cpp and why:
decode is llama.cpp's, prefill is ours by ~2x, and
since neither engine exceeds 39% of the measured
222 GB/s ceiling the decode gap is the execution
model rather than the kernels.
docs/edge/environment.md pins and machine, and the correction below.
On "the modified vllm": there isn't one, and the doc says so plainly because it
is the obvious assumption. The vLLM checkout is clean upstream (0 commits
ahead, 0 uncommitted) and the installed wheel is stock 0.28.0+cpu. This fork
changes vLLM's CPU *behaviour* through vLLM's own extension points at import
time -- three CPU patches in vllm_omni/patch.py, the quantization-config
registry, the IR op registry, platform hooks -- specifically so the changes
survive a wheel upgrade rather than being lost on it. One of those patches is
why "KV sizing is not a memory lever" looked true for months: CpuPlatform
overwrote kv_cache_memory_bytes unconditionally, so the setting had no effect.
There is no patch series to apply; pin the version and let patch.py do the rest.
.gitignore gains explicit exceptions: upstream's blanket `*.json` and `results/`
rules were silently eating the measurement artifacts, which in this fork are the
substance rather than build output. Binary captures stay ignored.
No vLLM or llama.cpp source is redistributed -- only output produced with them.
NOTICE records both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1ax5wYLA6XbqEL1i8L9gZ
110 KiB
ExecuTorch (pytorch/executorch) — Engine Analysis
| Field | Value |
|---|---|
| Local path | /data/zhoutaichang/embedding_infer/executorch |
| Remote | https://github.com/pytorch/executorch.git (origin) |
| Branch | main |
| HEAD | cfc6ecd5166bec70c5fde2bad836fed26018fe52 — "Qualcomm AI Engine Direct - Enable QNN Windows ARM64 build in CI (#22438)", 2026-09-07 23:47 +0800 |
| Declared version | 1.5.0 (version.txt) |
| Dirty status | clean (git status --short empty) |
| Submodules | all 23 uninitialized (git submodule status shows - for every entry), including extension/llm/tokenizers, backends/xnnpack/third-party/{XNNPACK,pthreadpool,cpuinfo,FP16,FXdiv}, backends/mlx/third-party/mlx, backends/vulkan/third-party/*, third-party/{ao,flatbuffers,flatcc,gflags,googletest,json,pocketfft,pybind11,ios-cmake,prelude}, kernels/optimized/third-party/eigen, shim, backends/cadence/utils/FACTO |
| Analysis date | 2026-09-08 (read-only; no builds, installs or GPU use) |
| Repo guidance followed | CLAUDE.md (AGENTS.md is a symlink to it), .claude/*.md reference docs, .wiki/index.md |
Evidence labels: [Verified] read in code/docs at this HEAD; [Inferred] reasoned from structure; [Proposal] design suggestion; [Unknown] not determinable from the tree. Repo-published numbers are labelled "repo-reported, not reproduced".
0. Summary
- What it is [Verified]: ExecuTorch is an ahead-of-time (AOT) compiler + minimal C++ execution runtime for PyTorch models. Python side (
exir/) lowerstorch.exportprograms through Edge IR, partitions subgraphs to hardware "delegates" (to_backend), plans memory statically, and serializes a FlatBuffer.pte(schema/program.fbs). The C++ runtime (runtime/executor/method.cpp) walks a linear instruction list per method. - Execution runtime, not a serving stack [Verified]: the core is a single-
Method, single-thread, sequential interpreter (Method::executeloops chains → instructions; comment says "Chains are executed sequentially today"). There is no request queue, batcher or server inruntime/. - Serving-adjacent layers exist above the core [Verified]:
extension/llm/runner/(TextLLMRunner / MultimodalRunner with token callbacks), an experimentalextension/llm/batching/(DecodeFirstScheduler,Runner,Session,GenerationHandlewith cancel) andexamples/llm_server/(OpenAI-compatible Python control plane over JSONL worker binaries). No in-tree class implementsbatching::Executoroutside its tests. - Backends in tree [Verified]: XNNPACK, Vulkan, WebGPU, Qualcomm QNN (HTP/GPU/LPAI), MediaTek Neuron, Samsung Exynos (ENN), Arm (Ethos-U via TOSA/Vela, VGF, Cortex-M CMSIS-NN), Cadence DSP, NXP Neutron, OpenVINO, Apple CoreML, Apple Metal (experimental), Apple "Core AI" (under construction), MLX (experimental), CUDA (AOTInductor + Triton; experimental ROCm), a "native" portable delegate, and the AOTI common library. No
backends/apple/mpsdirectory exists at this HEAD. - Device model [Verified]: backend selection happens entirely at export (partitioner). At runtime
BackendDelegate::Initlooks up the delegate by string id in a 16-slot static registry (kMaxRegisteredBackends = 16, runtime/backend/interface.cpp) and callsis_available(). There is no runtime device discovery/selection. - Memory [Verified]: static memory planning in Python (
greedydefault, optional banked planning) producesnon_const_buffer_sizes; the runtimeMemoryManager= method allocator +HierarchicalAllocator(planned arenas, ≤16 spans) + temp allocator. A newDevice{CPU,CUDA}concept andDeviceAllocatorregistry let planned buffers live in CUDA memory with explicit_h2d_copy/_d2h_copyops inserted at delegate boundaries. - Kernels [Verified]: portable (209 ops in
functions.yaml), optimized (23), quantized (19), ATen wrapper (225) plus LLM custom ops (custom_sdpa,update_cache,quantized_moe_ffn,gated_delta_rule, ...). Kernel table is a static array (default 250 ops × 8 kernels), selective build generatesselected_max_kernel_num.h. - Quantization [Verified]: torchao source transforms (
8da4w,int8,4w,torchao:8da*,torchao:fpa*) and PT2E quantizers per backend (XNNPACK, QNN, CoreML, Vulkan, OpenVINO, TOSA/Ethos-U/VGF). CUDA backend has int4/5/6/8 packed GEMM shims; MLX has int2/4/8 weight quant. - LLM decode loop [Verified]:
TextPrefiller::prefill(chunked) →TextTokenGenerator::generate→ per-tokenTextDecoderRunner::step→Module::execute("forward"|"decode")→Sampler→ tokenizerdecode→token_callback. KV cache is a mutable buffer inside the.pte(static,[B,H,max_context,D]layout), updated byupdate_cache/custom_sdpa. - TTS in tree [Verified]:
examples/models/voxtral_tts(LM + flow head + codec decoder, 5 methods, CPU/XNNPACK and CUDA, streaming codec chunks withAudioChunkCallback) andexamples/models/supertonic(MLX, 4 methods incl.vocoder, dynamic FP16). Mimi codec on QNN underexamples/qualcomm/oss_scripts/moshi. Audio front end:extension/audio/mel_spectrogram.py(exportable Whisper mel),extension/asr/runner(seq2seq + transducer),extension/llm/runner/audio.h. - VLA in tree [Verified]: none. No OpenVLA/pi0/LeRobot/diffusion-policy/proprioceptive references anywhere. Vision encoders exist for VLMs (LLaVA, Llama 3.2 Vision, Gemma3, SmolVLM, InternVL3) via
MultimodalRunnerwithvision_encoder/token_embedding/text_decodermethod naming. - Edge profile [Verified]: core runtime CI thresholds ~45–52 KB stripped (Linux gcc/clang size_test), ~118 KB bare-metal Cortex-M / ~136 KB Zephyr all-in; CMake presets for android-arm64-v8a, ios, macos, linux, windows, arm-baremetal, arm-ethosu-linux, esp-baremetal, riscv64-linux, zephyr, llm-release-{cuda,rocm,metal}, mlx-release. Android AAR (Maven), iOS xcframeworks (SwiftPM), Python wheels incl. aarch64 CUDA wheels.
- Jetson [Unknown]: no explicit Jetson/Orin documentation; an aarch64-Linux CUDA wheel workflow exists (.github/workflows/build-wheels-cuda-aarch64-linux.yml), so aarch64+CUDA is at least built.
- Tokenizers [Verified]: C++ tokenizers (HF JSON, Tiktoken, SentencePiece, Llama2c, Tekken) live in the
extension/llm/tokenizerssubmodule, which is not checked out here; only the.claude/tokenizers.mdsummary and call sites could be inspected.
1. Architecture overview
graph TD
subgraph AOT["AOT (Python, exir/)"]
TE["torch.export → ExportedProgram"] --> TOEDGE["to_edge / to_edge_transform_and_lower<br/>EdgeProgramManager"]
TOEDGE --> PART["Partitioner(s) → to_backend()<br/>BackendDetails.preprocess → delegate blob"]
PART --> PASSES["edge_to_executorch_passes<br/>ConstraintBasedSymShapeEvalPass, reinplace, PropagateDevicePass"]
PASSES --> MP["MemoryPlanningPass (greedy / banked)"]
MP --> EMIT["_Emitter → ExecutionPlan(FlatBuffer)"]
EMIT --> PTE[".pte (+ .ptd via flat_tensor)"]
end
subgraph RT["Runtime (C++, runtime/)"]
PTE --> DL["DataLoader (file/mmap/buffer)"]
DL --> PROG["Program::load → Program::load_method"]
PROG --> METH["Method::init<br/>parse_values / resolve delegates / resolve_operator"]
METH --> EXEC["Method::execute → execute_instruction<br/>KernelCall | DelegateCall | JumpFalse | Move | Free"]
EXEC --> KREG["Kernel registry<br/>portable / optimized / quantized / custom"]
EXEC --> BREG["Backend registry (≤16)<br/>BackendInterface::init/execute"]
MM["MemoryManager<br/>method alloc + HierarchicalAllocator + temp"] --> METH
DEV["DeviceAllocatorRegistry<br/>CPU / CUDA"] --> MM
PAL["Platform PAL (log, clock, alloc, abort)"] --> EXEC
end
subgraph EXT["Extensions"]
MOD["extension/module::Module"] --> PROG
LLM["extension/llm/runner<br/>TextLLMRunner / MultimodalRunner"] --> MOD
BATCH["extension/llm/batching<br/>DecodeFirstScheduler / Runner"] -.-> LLM
ASR["extension/asr/runner"] --> MOD
THR["extension/threadpool (pthreadpool+cpuinfo)"] --> KREG
THR --> BREG
BIND["android JNI / apple Swift / pybindings / wasm"] --> MOD
SRV["examples/llm_server (OpenAI HTTP)"] -.-> LLM
end
subgraph BE["Delegates (backends/)"]
BREG --> XNN["XNNPACK"] & VK["Vulkan"] & QNN["Qualcomm QNN"] & CML["CoreML"] & MLX["MLX"] & CUDA["CUDA (AOTI)"] & ARM["Arm Ethos-U / VGF"] & OTH["MediaTek / Samsung / NXP / OpenVINO / Cadence / WebGPU / Metal / native"]
end
Key modules
| Module | Path | Responsibility | Key symbols |
|---|---|---|---|
| Export IR | exir/program/_program.py | to_edge, to_edge_transform_and_lower, EdgeProgramManager.to_backend/to_executorch, ExecutorchProgramManager.buffer/save |
to_edge (L1414), EdgeProgramManager (L1496), ExecutorchProgramManager (L1823) |
| Backend API | exir/backend/backend_api.py, exir/backend/partitioner.py, exir/backend/backend_details.py | Partition + preprocess into delegate blobs | to_backend (singledispatch), Partitioner.partition, BackendDetails.preprocess/preprocess_multimethod, PreprocessResult |
| Memory planning | exir/memory_planning.py, exir/banked_memory_planning.py | Lifetime analysis, arena assignment | greedy, naive, MemoryPlanningAlgorithmSuite, _partition_specs_by_device, TargetMemoryMap |
| Config | exir/capture/_config.py | Export/backend knobs | EdgeCompileConfig, ExecutorchBackendConfig |
| Emitter | exir/emit/_emitter.py | FX → FlatBuffer instructions | _Emitter, _TopLevelEmitter |
| Schema | schema/program.fbs | .pte file format |
Program, ExecutionPlan, Chain, KernelCall, DelegateCall, NonConstBufferDevice |
| Program/Method | runtime/executor/program.h, runtime/executor/method.cpp | Load, init, execute | Program::load/load_method, Method::init/execute/step/set_input/get_outputs, BackendDelegate |
| Memory | runtime/executor/memory_manager.h, runtime/core/hierarchical_allocator.h, runtime/core/memory_allocator.h | Arena allocators | MemoryManager, HierarchicalAllocator::get_offset_address, MemoryAllocator |
| Device memory | runtime/core/device_allocator.h, runtime/core/device_memory_buffer.h, runtime/core/portable_type/device.h | Non-CPU planned buffers | DeviceAllocator, DeviceAllocatorRegistry, DeviceMemoryBuffer, Device{CPU,CUDA} |
| Backend registry | runtime/backend/interface.h | Delegate ABI | BackendInterface::{is_available,init,execute,set_option,get_option,destroy}, register_backend |
| Kernel registry | runtime/kernel/operator_registry.h | Op → kernel resolution | Kernel, KernelKey, register_kernels, get_op_function_from_registry |
| PAL | runtime/platform/platform.h | OS abstraction | et_pal_init/abort/current_ticks/emit_log_message/allocate/free, PalImpl |
| Kernels | kernels/portable/functions.yaml, kernels/optimized/optimized.yaml, kernels/quantized/quantized.yaml | CPU op libraries | YAML op defs + kernels/*/cpu/op_*.cpp |
| Selective build | tools/cmake/Codegen.cmake, codegen/gen.py | Generate registration for selected ops | gen_selected_ops, EXECUTORCH_SELECT_OPS_* |
| Module API | extension/module/module.h | Convenience wrapper | Module::{load_method,execute,forward,set_input,set_output,method_meta}, LoadMode |
| LLM runner | extension/llm/runner/ | Prefill/decode loop | IRunner, TextLLMRunner, TextPrefiller, TextDecoderRunner, TextTokenGenerator, IOManager, MultimodalRunner, MultimodalPrefiller, LLMSession, LLMEngine |
| LLM batching | extension/llm/batching/ | Experimental multi-session scheduler | Scheduler, DecodeFirstScheduler, Executor, Runner, Session, GenerationHandle |
| LLM cache | extension/llm/cache/ | Off-graph KV cache abstractions | CacheBase, SequenceCache, CellCache, RingPolicy, kvcache::update_and_attend |
| Custom ops | extension/llm/custom_ops/custom_ops.py | LLM kernels | sdpa_with_kv_cache, custom_sdpa, update_cache, quantized_moe_ffn, gated_delta_rule |
| Threadpool | extension/threadpool/threadpool.h, runtime/kernel/thread_parallel_interface.h | CPU parallelism | ThreadPool, get_threadpool, get_pthreadpool, parallel_for |
| Devtools | devtools/etdump/, devtools/inspector/_inspector.py, devtools/bundled_program/ | Profiling / debugging | ETDump, Inspector, bundled program |
2. Device layer
2.1 Backends actually in tree [Verified]
Every backend registers itself with register_backend(...) into a static 16-entry table (runtime/backend/interface.cpp, kMaxRegisteredBackends = 16, with a TODO noting global statics prevent multiple executors). Registration sites:
| Backend id / dir | Registration | Hardware / OS | Status note |
|---|---|---|---|
| XNNPACK — backends/xnnpack | XNNPACKBackend.cpp#L272 | ARM64/ARMv7/x86 CPU on Android, iOS, macOS, Linux, Windows (xnnpack-overview.md); KleidiAI via EXECUTORCH_XNNPACK_ENABLE_KLEIDI (CMakeLists.txt#L34) |
default CPU delegate |
| Vulkan — backends/vulkan | VulkanBackend.cpp#L931 | Android GPUs (Adreno/Mali/PowerVR noted in wiki), desktop experimental | mature on Android |
| WebGPU — backends/webgpu | WebGPUBackend.cpp#L263 | Dawn/Tint WGSL; browser via Emscripten, native Metal/Vulkan | under active development |
| Qualcomm QNN — backends/qualcomm | QnnExecuTorchBackend.cpp#L394 | HTP / GPU / LPAI; 38 chipset enum entries in qc_schema.py (QcomChipset) |
no BC guarantees stated |
| MediaTek Neuron — backends/mediatek | NeuronBackend.cpp#L261 | Dimensity 9300/9400 NPU | |
| Samsung Exynos — backends/samsung | enn_backend.cpp#L96 | Exynos 2500 NPU/DSP (EXECUTORCH_BUILD_ENN) |
|
| Arm Ethos-U — backends/arm | EthosUBackend.cpp#L451 | Ethos-U55/65/85 via TOSA → Vela | bare-metal |
| Arm VGF — same dir | VGFBackend.cpp#L803 | Vulkan ML extensions (SPIR-V ML) | |
| Arm Cortex-M — backends/cortex_m | not a delegate: an operator dialect (cortex_m::*) on CMSIS-NN with portable fallback |
Cortex-M MCUs | Beta |
| Cadence — backends/cadence | no delegation; custom ops for HiFi4/FusionG3/Vision DSP (see wiki "No-delegation architecture") | Xtensa DSP | |
| NXP Neutron — backends/nxp | NeutronBackend.cpp#L676 | eIQ Neutron NPU (i.MX) | |
| OpenVINO — backends/openvino | OpenvinoBackend.cpp#L419 | Intel CPU / iGPU / dGPU / NPU | |
| Apple CoreML — backends/apple/coreml | coreml_backend_delegate.mm#L438 | iOS/macOS ANE/GPU/CPU (compute units via compile spec) | |
| Apple Metal — backends/apple/metal | metal_backend.cpp#L696 | AOTI-driven Metal | "EXPERIMENTAL" |
| Apple Core AI — backends/apple/coreai | Python only at this HEAD | "Under construction — not for use" | |
| MLX — backends/mlx | MLXBackend.cpp#L570 | Apple Silicon GPU, macOS ≥ 14, iOS ≥ 17 experimental (mlx-overview.md) | experimental |
| CUDA — backends/cuda | cuda_backend.cpp#L1429 | NVIDIA Linux/Windows via AOTInductor + Triton; ROCm experimental (rocm.md) | |
| AOTI common — backends/aoti | library, not a backend | shared shims for CUDA/Metal | |
| native — backends/native | NativeBackend serializes an fx subgraph to a generic flatbuffer executed by backends/native/runtime/Program.h |
portable | new |
| example/test — backends/example, backends/test | reference delegate + test harness |
Reference table also in .claude/backends.md and docs/source/backends-overview.md.
2.2 Device discovery / selection [Verified]
- AOT: the user chooses the target by passing a
Partitionertoto_backend()/to_edge_transform_and_lower(). Multiple partitioners are applied in priority order; unmatched ops stay asKernelCalls for the CPU kernel library (.claude/backends.md). - Runtime:
BackendDelegate::Init(method.cpp#L61) doesget_backend_class(backend_id)thenbackend->is_available()and fails withNotFoundif either check fails. There is no fallback to another backend at runtime and no enumeration of physical devices. Backend-specific selection (e.g., CoreML compute units, QNN SoC/HTP arch, CUDA graph enable) is carried inCompileSpecs embedded in the.pteand parsed ininit(e.g., coreml_backend_delegate.mm#L278,kEnableCudaGraphForMethodin cuda_backend.cpp#L85). - Runtime knobs can be pushed to a backend by name with
set_option(backend_name, Span<BackendOption>)(interface.h); aBackendOptionsMapcan be passed atload_methodtime.
2.3 Memory allocation / pools [Verified]
MemoryManager(method_allocator, planned_memory, temp_allocator)(memory_manager.h); method and temp allocator must differ.planned_memoryis aHierarchicalAllocatoroverSpan<Span<uint8_t>>— one span permem_id, hard capkSpanArraySize = 16for the deprecated constructor;get_offset_address(memory_id, offset, size)with overflow checks (hierarchical_allocator.h).- Sizes come from
ExecutionPlan.non_const_buffer_sizes(program.fbs#L368); the app must allocate those arenas.Method::resolve_operatoruses temp allocator (falls back to method allocator) forTensorMetascratch (method.cpp#L731). - Temp allocator is reset after each instruction and at the start of
execute()(method.cpp#L1697) — kernels get per-op scratch. - Planning algorithms:
greedy(default, allows overlapping lifetimes),naive,MemoryPlanningAlgorithmSuitepicks the minimum total;banked_memory_planning.TargetMemoryMapadds capacity-aware placement across memory banks with spill (banked_memory_planning.py). Mutable buffers (KV caches) are placeholders that are "never freed" (memory_planning.py#L492) and are planned only ifalloc_mutable_buffers. - Inputs/outputs: if not memory-planned,
Method::set_inputaliases user memory (share_tensor_data) instead of copying; otherwisecopy_tensor_dataafterresize_tensor(method.cpp#L1146);set_output_data_ptrlets the caller supply output buffers.
2.4 Host ↔ device transfers [Verified]
Device{type: CPU|CUDA, index}(device.h);DeviceAllocatorhasallocate/deallocate/copy_host_to_device/copy_device_to_host;DeviceAllocatorRegistryholds one allocator per device type;DeviceMemoryBuffer::create(size, type)RAII (device_allocator.h, device_memory_buffer.h).ExecutionPlan.non_const_buffer_device: [NonConstBufferDevice]marks planned arenas on CUDA (program.fbs#L417);MemoryManager::has_device_memory()exposes this.- At export,
ExecutorchBackendConfig.enable_non_cpu_memory_planning=True(default) partitions specs by device (_partition_specs_by_device) andPropagateDevicePassinserts_h2d_copy/_d2h_copyops at delegate boundaries (_config.py#L126, propagate_device_pass.py, _device_copy_ops_registry.py#L27). - CUDA backend auto-registers
CudaAllocator(cuda_backend.cpp#L1431, cuda_allocator.h) and usescudaMemcpyAsyncdevice-to-device for CUDA-graph static I/O staging (cuda_backend.cpp#L574). - Other delegates copy internally (XNNPACK:
xnn_setup_runtime_v2on external tensors; Vulkan:maybe_resize_input/staging in VulkanBackend.cpp#L529; QNN:RegisterMem/PreRegisterMemshared buffers and rpcmem in QnnManager.h#L89, rpc_mem.h).
2.5 Synchronization / streams [Verified]
- Core runtime is synchronous: every
KernelCall/DelegateCallreturns before the next instruction. No stream abstraction inruntime/. - CUDA backend: optional CUDA-graph capture per method (
CudaGraphPhase::{Warmup,Replay,Disabled},kCudaGraphWarmupSteps,cudaGraphLaunchon a stream — cuda_backend.cpp#L434); enable with compile-specenable_cuda_graph_for_method. Unavailable on ROCm (rocm.md). - Vulkan:
ComputeGraph::submit_current_cmd[_and_wait],submit_deferred_cmds_and_wait,prepack,optional_warmup_execute,propagate_resize(ComputeGraph.h#L1103). - MLX:
async_evalwith keep-alive of input data (MLXBackend.cpp#L94).
2.6 Portability [Verified]
- PAL functions are weak symbols (
ET_INTERNAL_PLATFORM_WEAKNESS) or aPalImpltable (platform.h); defaults inruntime/platform/default. Examples of custom PALs: Arduino (examples/arduino/arduino_pal.cpp), Zephyr (zephyr/), CUDA (backends/cuda/runtime/platform/platform.cpp). - Build presets (CMakePresets.json):
android-arm64-v8a,android-x86_64,macos,ios,ios-simulator,linux,windows,pybind,zephyr,arm-baremetal,arm-ethosu-linux,esp-baremetal,riscv64-linux,llm,llm-release[-cuda|-rocm|-metal],llm-debug[-cuda|-metal|-vulkan],mlx[-release|-debug],profiling. - Host OS for building: Linux x86_64, macOS x86_64/ARM64, Windows 10+ (VS2022 + Clang-CL) or WSL (using-executorch-building-from-source.md).
3. Kernel layer
3.1 Operator set & registration [Verified]
- Op definitions are YAML (
- op:/- func:entries): portable 209 (functions.yaml) + 2 custom (custom_ops.yaml); optimized 23 (optimized.yaml —op_add/bmm/div/elu/exp/fft_*/gelu/grid_sampler_2d/le/linear/log_softmax/mm/mul/native_layer_norm/sub/sum/where); quantized 19 (quantized.yaml); ATen wrapper 225 (kernels/aten/functions.yaml); prim ops (et_copy_index,et_view, sym ops) in kernels/prim_ops/register_prim_ops.cpp. - Codegen (codegen/gen.py, codegen/templates) emits
Kernel{name, KernelKey, OpFunction}arrays registered viaregister_kernels(Span<const Kernel>)(operator_registry.h#L179).KernelKeyencodes dtype/dim-order per tensor arg (kKernelKeyBufSize = 659); a null key is the fallback kernel. - Table capacity:
MAX_KERNEL_NUM→EXECUTORCH_SELECTED_MAX_KERNEL_NUM(generated by selective build) → default250 ops × 8 kernels(operator_registry.cpp#L30). Duplicate registration aborts (see .claude/faq.md). - Portable kernels must use ExecuTorch types (
executorch::aten::Tensor, noat::Tensor) — kernels/README.md.
3.2 Dispatch [Verified]
- At
Method::init, eachKernelCallinstruction is resolved once:resolve_operatorbuildsTensorMetafor the args and callsget_op_function_from_registry(name, meta_list); the resultingOpFunctionpointer is stored inchain.kernels_[instr](method.cpp#L731, #L1040). - At
execute_instruction,KernelCallinvokeschain.kernels_[idx](context, args)with aKernelRuntimeContext(event_tracer, temp_allocator);DelegateCallinvokesdelegates_[idx].Execute(BackendExecutionContext, args);JumpFalseCall,MoveCall,FreeCallimplement control flow, aliasing and explicit frees (method.cpp#L1450). - Routing to a backend is therefore decided entirely at export: an op either became part of a delegate blob (
executorch_call_delegate→DelegateCall) or stays a CPUKernelCall. There is no runtime per-op dispatcher across backends. - When the
Methodis loaded with an explicitkernel_registryspan, resolution uses that list instead of the global table (method.hSpan<const Kernel> kernel_registry_).
3.3 Quantization formats & where dequant happens [Verified]
- LLM source transforms (quantize.py):
int8(weight-only),8da4w/8da8w(int8 dynamic act + int4/int8 weight, torchaoInt8DynamicActivationIntxWeightConfig),4w,torchao:8da*andtorchao:fpa*low-bit kernels for Arm, embedding quant4,32etc. Config in llm_config.py (QuantizationConfig.qmode,pt2e_quantize,use_qat,use_spin_quant,preq_modevalues8da4wand8da4w_output_8da8w). SpinQuant R1/R2 rotations: apply_spin_quant_r1_r2.py. - KV-cache quant:
QuantizedKVCache(int8 per-token,QuantizedCacheType),StaticQuantizedKVCachewith calibration,QuantizedRingKVCache,CustomKVCacheWithAttentionSink(custom_kv_cache.py);QuantizedSDPAconsumes int8 caches (sdpa.py#L104). Dequant happens insidecustom_quantized_sdpa/XNNPACK kernels at runtime. - PT2E quantizers (quantizer_lib.py): XNNPACK (xnnpack_quantizer.py), QNN (
get_qnn_quantizer, 8a8w/16a4w/16a8w — see wiki recipes.md), CoreML, Vulkan, OpenVINO, TOSA/Ethos-U/VGF, Cortex-MCortexMQuantizer. Q/DQ nodes are fused into the delegate blob by the backend's preprocess; ops that stay on CPU runquantized_decomposed::*kernels from kernels/quantized (seequant_fusion_pass.pyin exir/passes). - GPU: CUDA int4 tile-packed (
tile_packed_to_4d,_weight_int4pack_mm) plus int5/int6/int8 planar GEMM shims (backends/cuda/runtime/shims, quantize_op_dispatch); MLX INT2/4/8 weight quant via torchao (mlx-overview.md); WebGPU 4-bit weight-only, 8da4w, q8ta (webgpu README). Alsoextension/llm/export/{int4,mx,nvfp4,gguf}.pyfor GGUF loading, MX and NVFP4 formats.
3.4 Compilation / JIT / graph capture [Verified]
- Everything is AOT:
torch.export→to_edge(ATen→Edge dialect, decompositions, dim-order) → partition/preprocess →to_executorch(passes incl.ConstraintBasedSymShapeEvalPass,reinplace,MemoryPlanningPass,ToOutVarPass) → emit. No JIT at runtime. - Delegate compilers: XNNPACK builds an
xnn_subgraphatinit(xnn_create_runtime_v4with pthreadpool, weights cache, workspace — XNNCompiler.cpp#L2296); Vulkan buildsComputeGraphfrom a flatbuffer, prepacks and optionally warms up (VulkanBackend.cpp#L676); QNN loads precompiled context binaries (or online-prepare) viaQnnManager::Compile(QnnManager.h#L85); CoreML compiles/caches.mlmodelcat first load; CUDAdlopens the AOTInductor.sofrom the.ptdand callsAOTInductorModelContainerCreateWithDevice/Run(cuda_backend.cpp#L164); MLX interprets its own flatbuffer bytecode (backends/mlx/serialization).
3.5 Fallback path [Verified]
- Unsupported ops simply remain
KernelCalls to the portable/optimized CPU kernels (backends-overview.md: "Operators not supported by the delegate are executed using the portable CPU fallback"). If the CPU kernel is also missing (selective build),Method::initfails withOperatorMissing(0x14) (.claude/faq.md). Cortex-M's dialect explicitly relies on portable-ops fallback (cortex_m/README.md). extension/llm/custom_ops/op_fallback.cppprovides an AOT-side fallback op (op_fallback.py).
3.6 Extension points [Verified]
- Custom op: define via
torch.library(see custom_ops.py), implement anout-variant C++ kernel using ExecuTorch tensors, register withEXECUTORCH_LIBRARY/ YAML + codegen (gen_selected_ops, Codegen.cmake); docs kernel-library-custom-aten-kernel.md, examples/portable/custom_ops. - Custom backend: Python
BackendDetails.preprocess(edge_program, compile_specs) -> PreprocessResult+ aPartitionerreturningPartitionResult(backend_details.py#L53, partitioner.py#L37); C++BackendInterfacesubclass +static auto _ = register_backend({"Id", &instance}); template in docs/source/backends/template and the backends/example reference delegate. Multi-method delegates viapreprocess_multimethodandMethodProgramsPartitionerSpec(backend_api.py#L664). - Custom memory planning: pass your own
MemoryPlanningPass/algorithm list or custommem_idpools (compiler-memory-planning.md). - Custom passes:
EdgeProgramManager.transform(passes)andExecutorchBackendConfig.passes(compiler-custom-compiler-passes.md).
4. Model runner
4.1 Loading / export / conversion pipeline [Verified]
- Formats:
.pte(FlatBuffer program with inline or segmented constants and delegate blobs; extended header in schema/extended_header.h; pte-file-format.md);.ptdexternal tensor files (extension/flat_tensor,FlatTensorDataMap; ptd-file-format.md) used for external constants (ExecutorchBackendConfig.external_constants), CUDA AOTI blobs (aoti_cuda_blob.ptd), Whisper weights, LoRA adapters. - Export API:
to_edge_transform_and_lower(ep, partitioner=[...], transform_passes=..., compile_config=EdgeCompileConfig)then.to_executorch(ExecutorchBackendConfig)and.buffer/.save()(_program.py#L1280). Alsoexecutorch.exportrecipe API (export/). - LLM export:
python -m executorch.extension.llm.export.export_llm(Hydra/YAMLLlmConfig:base,model,export,quantization,backend,debug) → export_llm.py → export_llama_lib.py →LLMEdgeManagerin builder.py. Model classes inModelType(stories110m, llama2/3/3_1/3_2, llama3_2_vision, static_llama, qwen2_5*, qwen3*, qwen3_5*, phi_4_mini, smollm2, lfm2*). HF models via optimum-executorch (out of tree; export-llm-optimum.md). QNN LLMs use a separate path examples/qualcomm/oss_scripts/llama/llama.py (wiki warns againstexport_llama --qnn, model-specific.md). - Loading:
DataLoadervariants (FileDataLoader,MmapDataLoaderwith mlock/madvise,BufferDataLoader,FileDescriptorDataLoader— extension/data_loader);Module::LoadMode {File, Mmap, MmapUseMlock, MmapUseMlockIgnoreErrors, MmapUseMadvise}(module.h#L50); LLM runner defaultMmapUseMlockIgnoreErrors(llm_runner_helper.h#L106).
4.2 Execution lifecycle [Verified]
Program::load(loader, Verification)→ header check, optional flatbuffer verification (program.h#L97).Program::load_method(name, MemoryManager*, EventTracer*, NamedDataMap*)→Method::init:parse_values(materializevalues_table, tensors pointing into planned arenas / constant segments / external.ptddata), allocate andBackendDelegate::Initeach delegate (callsbackend->init→DelegateHandle*),resolve_operatorfor everyKernelCall(method.cpp#L883).Method::set_input(s)(resize + copy or alias) →Method::execute()(checks all inputs set, resets temp allocator, sequentially runs all chains/instructions, guards against >ET_MAX_INSTRUCTIONS=10 M, thenreset_execution) →Method::get_outputs(shallow copies) (method.cpp#L1697).- Alternative:
Method::step()executes one instruction (experimental; used by devtools/debuggers),reset_execution()afterEndOfMethod. - Warmup: no core warmup phase;
TextLLMRunner::warmup(prompt, max_new_tokens)runs a throwaway generation (text_llm_runner.h#L146); Vulkanoptional_warmup_execute; CUDA-graph warmup steps. - Multiple
Methods per program (e.g.,encoder,text_decoder,prefill,decode,vision_encoder,token_embedding, TTSduration_predictor/text_encoder/vector_estimator/vocoder) are loaded independently, each with its own planned memory; a globalMergedDataMaplets them share.ptdconstants.
4.3 Dynamic shapes [Verified]
- Export with
torch.export.Dimdynamic shapes;ConstraintBasedSymShapeEvalPass(default inExecutorchBackendConfig.sym_shape_eval_pass) converts symbolic sizes to upper bounds for memory planning (DynamicMemoryPlanningMode.UPPER_BOUNDdefault;SYMBOLICexists) (sym_shape_eval_pass.py#L257, dynamic_shape.py, _config.py#L56). - Runtime tensors carry
TensorShapeDynamism {STATIC, DYNAMIC_BOUND, DYNAMIC_UNBOUND}(tensor_shape_dynamism.h);set_inputcallsresize_tensorthen copies;DYNAMIC_UNBOUNDis not memory-planned and must be provided by the caller. - Delegates handle dynamic shapes themselves: XNNPACK
xnn_reshape_external_value+xnn_reshape_runtime+resize_outputsper call (XNNExecutor.cpp#L150); Vulkanmaybe_resize_input/propagate_resize; MLX advertises dynamic shape support; QNN graphs are static (wiki known-issues). LLM examples default to static (enable_dynamic_shape=Falsein model_args.py#L102); the LLM runner readsenable_dynamic_shapemetadata (constants.h) and otherwise pads prefill to fixed length.
4.4 State & KV cache [Verified]
- In-graph mutable buffers:
KVCacheregistersk_cache/v_cachebuffers of shape[max_batch, n_heads, max_context_len, head_dim]and updates withindex_copy_/slice writes (attention.py#L99);CustomKVCacheusesllama::update_cache+llama::custom_sdpa(custom_kv_cache.py#L579, op_update_cache.cpp, op_sdpa.cpp). Ring buffer (CustomRingKVCache,QuantizedRingKVCache) and attention-sink variants exist;ReplaceKVCache...transforms are selected byModelConfigflags (use_kv_cache,use_sdpa_with_kv_cache,quantize_kv_cache,use_attention_sink,use_ring_kv_cache). - The cache lives in the method's planned non-const memory (mutable buffer, never freed) and persists across
execute()calls; the runner trackspos_and passesstart_posas an input tensor.reset()zeroes position; there is no paging, eviction or cross-request sharing in the core runner — oneMethod= one sequence.max_context_lenvsmax_seq_len(prefill chunk) are metadata methods (get_max_context_len,get_max_seq_len). - Static attention (NPU-friendly):
StaticKVCache/StaticAttention/StaticAttentionIOManagerin Python (static_attention.py) and C++ (static_attention_io_manager.h) keep caches outside the graph as method inputs/outputs; update stylesSHIFT_POINTERandSMART_MASK(fixed I/O pointers enabling persistent AP↔NPU mapping; supports per-layer cache lengths for sliding-window layers, batch 1, lookahead n-gram cache). Used by the QNN runner (kv_manager.h, rpc_mem.h). - Off-graph cache library (new, experimental):
extension/llm/cachedefinesCacheBase,SequenceControl::rewind,SequencePlanner::plan/commit,LayoutPolicy(FlatPolicy,RingPolicy),SequenceCache, and a paged-likeCellCache(cells claimed per position with owner bitsets,kMaxSeqs = 64,free_cells()), reached from the graph through the functional custom opkvcache::update_and_attendkeyed bylayer_id(cache.h, cell_cache.h, sequence_cache.h, update_and_attend.py). MLX supports per-session mutable state (mlx_mutable_state.h,skip_mutable_buffer_init— MLXBackend.cpp#L298); CUDA hascuda_mutable_state.h(backends/cuda/runtime/cuda_mutable_state.h). [Inferred] these are the substrate for the multi-sessionLLMEngineimplementations (Muse Glimmer, Qwen3.5-MoE, Gemma4-31B engines).
4.5 Multimodal stages [Verified]
MultimodalRunner(IRunner) takesstd::vector<MultimodalInput>(text / tokens /Image/Audio(mel) /RawAudio);MultimodalPrefillerrunsvision_encoderoraudio_encoder,token_embedding, thentext_decoderfor prefill; decode continues withMultimodalDecoderRunner/TextTokenGenerator(multimodal_runner.h, multimodal_prefiller.cpp#L112, multimodal_input.h).- Encoders/preprocessors as separate methods or
.ptes: LLaVA (export_image_encoder, CLIP preprocessing in Python; export_llava.py#L138); Llama 3.2 Vision (exportable preprocess + C++ preprocess in llama3_2_vision/preprocess); Whisper melpreprocessor.pte(extension/audio/mel_spectrogram.py); Voxtral Realtimepreprocessor.pte(voxtral_realtime README). - Decoders/vocoders: Voxtral TTS
codec_decoder.pte(Conv1d/ConvTranspose1d + 8 transformer layers → 24 kHz) driven byvoxtral_tts_runnerwith a 7-step Euler flow head; Supertonicvocodermethod; Mimi decoder on QNN (qnn_mimi_decoder_runner.cpp). - Diffusion:
examples/models/stable_diffusionandstable_diffusion_3_5_largeare model definitions/export scripts only (no runner); DiT on QNN (dit.py).
5. Scheduler
5(a) Request / token / pipeline-level scheduling
- Core runtime: N/A [Verified]. One
Methodexecutes one request synchronously;execute()refuses re-entry whilein_progress(); there is no queue, no batching across requests, no admission control. Batch size is baked into the exported shapes (LLM examples: batch 1). - LLM runner (
extension/llm/runner) [Verified]: sequential prefill → decode per call;GenerationConfig(max_new_tokens,seq_len,temperature,echo,warming,grammar) (irunner.h); chunked prefill inTextPrefiller::prefillwhen prompt >max_seq_len(text_llm_runner.cpp#L143); optional two-method PTE (prefill+decode) (constants.h#L28). Cancellation:IRunner::stop()sets an atomic checked per token inTextTokenGenerator::generate(text_token_generator.h#L131);LLMSession::stop()is documented as a token-boundary cooperative stop (llm_session.h#L104). No backpressure concept (callbacks are synchronous on the calling thread). extension/llm/batching(experimental) [Verified]:Schedulerinterface (submit/has_work/get_work/cancel(sid)/clear/max_prefill_chunk_size),DecodeFirstScheduler::create(max_batch_tokens=544, max_decode_sequences=32, max_prefill_chunk_size=256)takes all pending decodes first then fills the token budget with round-robin prefill chunks (continuous batching with chunked prefill; admission rejects over-budget tasks) (decode_first_scheduler.h);Executor(open_session/close_session/set_sampling/execute(BatchInput, BatchOutput));Runner(executor, scheduler)withopen_session_async,Session::generate_async(delta, GenConfig, callback)returningGenerationHandle{cancel, done, wait, finish_reason}andFinishReason {StopToken, NewTokenLimit, Cancelled, Failed}(runner.h). Gap [Verified]: no in-treeExecutorimplementation outsideextension/llm/batching/test; no batched.pteexample wires into it yet.examples/llm_server[Verified]: OpenAI-compatible/v1/chat/completions(streaming + non-streaming), one worker process with serialized execution, multiple isolated sessions on one weight load when the engine reportsLLMServingCapacity > 1, bounded request cancellation, structured 400s for unsupported params (n>1, penalties, logprobs...) (examples/llm_server/README.md, worker_loop.h, worker_prefill_plan.h).LLMEngineimplementations: muse_glimmer_engine.h (with DFlash speculative decoding), qwen35_moe_engine.h, gemma4_31b_engine.h.- Multi-stage pipelines (TTS/ASR): hand-written in each example runner (e.g.,
VoxtralTtsRunner::synthesize_streamingLM → flow head → codec chunks withstreaming_chunk_frames_=25,streaming_initial_chunk_=5,streaming_left_context_=25— voxtral_tts_runner.h#L121); QNN has a Pythongenai_pipelinewith model-preparation / quantization / compilation / inference stages (genai_pipeline.py).
5(b) Graph / operator / thread-level scheduling
- Op ordering [Verified]: fixed at export (topological order emitted into
Chain.instructions); executed strictly sequentially byMethod::execute— "Chains are executed sequentially today, but future async designs may branch and run many in parallel" (method.cpp#L1722). No inter-op parallelism, no stream assignment in the core. - Intra-op threading [Verified]:
extension/threadpoolwrapspthreadpooland sizes the pool fromcpuinfoperformant cores (get_num_performant_cores, cpuinfo_utils.cpp);ThreadPool::run(fn, range)is blockingpthreadpool_parallelize_1d;_unsafe_reset_threadpool(n)changes size (threadpool.cpp#L65); kernels useparallel_for(begin,end,grain,fn)which degrades to a serial loop withoutET_USE_THREADPOOL(thread_parallel_interface.h#L53). XNNPACK shares the sameget_pthreadpool()(XNNCompiler.cpp#L2300).NoThreadPoolGuardprevents nested parallelism. - Delegate-internal scheduling [Verified]: Vulkan builds command buffers per graph and submits/waits (
submit_current_cmd_and_wait); CUDA optionally replays a captured CUDA graph; QNN executes a named graph on HTP with optional multi-graph context; XNNPACK runs the subgraph on the pthreadpool. - Batching [Verified]: static batch dim only;
HierarchicalAllocatorsizes are per-method so batch changes require re-export.
6. I/O layer
- Text tokenization [Verified via docs, submodule absent]:
extension/llm/tokenizers(pytorch-labs tokenizers, submodule commit1d7ca636) provides C++HFTokenizer(tokenizer.json),Tiktoken,SentencePiece,Llama2c,Tekkenplus Pythonpytorch_tokenizers.get_tokenizer(.claude/tokenizers.md);llm::load_tokenizer(path, special_tokens)auto-detects (llm_runner_helper.h#L47); llama runner tries Tiktoken → SentencePiece → Llama2c (runner.cpp). WASM tokenizers bindings in extension/wasm/tokenizers. - Image preprocessing [Verified]: C++
ImageProcessor::process/process_yuv/process_into(resize + normalize, NV12/NV21, SIMD and Apple GPU variants) in extension/image/image_processor.h; SwiftExecuTorch+ImageProcessor.swift; JNIjni_layer_image.cpp; examples usestb_image(gemma3/e2e_runner.cpp) andllm::Image(image.h); custom optile_crop(op_tile_crop.cpp). - Audio [Verified]:
WhisperAudioProcessor(torch.stft + mel filterbank, exportable towhisper_preprocess.pte, 16 kHz/80 bins/30 s defaults) in extension/audio/mel_spectrogram.py;RawAudio{data, batch, n_channels, n_samples}andAudio(mel, float/bf16) types (audio.h);wav_loader.h; ASR runnersSeq2SeqRunner(Whisper-styleencoder+text_decoder) andTransducerRunner(Parakeet TDT) in extension/asr/runner; Silero VAD streaming runner (stream_main.cpp). - Streaming [Verified]: token-level
token_callback(const std::string&)per generated token andstats_callback(const Stats&)(first_token_ms,prompt_eval_end_ms,num_generated_tokens,gpu_total_bytes— stats.h); audio-chunkAudioChunkCallbackin Voxtral TTS; Voxtral Realtime processes 80 ms chunks live; Supertonic persistent mode emits JSONL results per request. HTTP SSE streaming inexamples/llm_server. - Buffering [Verified]: planned arenas allocated by the app;
Moduleallocates them internally;set_output_data_ptrfor zero-copy outputs; XNNPACK weights cache for.ptdweights (XNNCompiler.cpp#L2283). - Postprocessing [Verified]:
Sampler(temperature, top-ksample_topk, argmax) +LogitProcessorchain (grammar hooks) (sampler.h, logit_processor.h); tokenizerdecode(prev, cur); WAV writers in TTS examples (wav_writer.h). - Application boundaries [Verified]:
- C++:
runtime/core API,extension/module::Module,extension/tensor(TensorPtr),extension/runner_util, LLM/ASR runners,executor_runnerCLI (examples/portable/executor_runner/executor_runner.cpp). - Python:
executorch.runtime.Runtime/Program/Methodandportable_lib._load_for_executorch(.claude/runtime-api.md, extension/pybindings/pybindings.cpp); LLM runner pybindings (extension/llm/runner/pybindings.cpp). - Android: Kotlin
Module,Tensor,EValue,ExecuTorchRuntime,BackendOptionsMap+ JNI layers for runtime/llama/asr/image/training (extension/android, jni_layer_llama.cpp); shipped as AAR on Maven Central (using-executorch-android.md). - Apple: Objective-C/Swift
ExecuTorchModule/Tensor/Value/ImageProcessor/BackendOptions(extension/apple/ExecuTorch/Exported) andExecuTorchLLM(extension/llm/apple); xcframeworks via SwiftPM (using-executorch-ios.md). - WASM: extension/wasm/wasm_bindings.cpp.
- HTTP:
examples/llm_server(Python control plane; C++ single-binary server listed as future). - Demo apps in tree: only examples/demo-apps/react-native/rnllama (native Android/iOS demo apps have moved out of this tree; docs still reference them).
- C++:
7. Execution flow
7(a) Text LLM decode (TextLLMRunner over XNNPACK-delegated .pte) [Verified]
sequenceDiagram
participant App
participant R as TextLLMRunner
participant Tok as Tokenizer
participant P as TextPrefiller
participant G as TextTokenGenerator
participant D as TextDecoderRunner
participant M as Module/Method
participant K as Kernels / XNNPACK delegate
participant S as Sampler
App->>R: create_text_llm_runner(pte, tokenizer, data_files)
App->>R: load()
R->>M: load_method("forward") → Method::init (parse_values, BackendDelegate::Init → XNNPACKBackend::init builds xnn runtime, resolve_operator)
App->>R: generate(prompt, GenerationConfig, token_cb, stats_cb)
R->>Tok: encode(prompt, bos, eos)
R->>P: prefill(tokens, pos_) (chunked ≤ max_seq_len)
P->>D: step(tokens_tensor, start_pos)
D->>M: IOManager::prepare_prefill → execute("forward", {tokens, start_pos})
M->>K: Method::execute: KernelCall/DelegateCall sequence (update_cache, custom_sdpa, xnn subgraph)
K-->>M: logits
M-->>D: logits tensor
D->>S: logits_to_token(temperature)
S-->>P: first token
P-->>R: first token, pos_ += n
R->>G: generate(first token, start_pos, max_new_tokens, temperature, token_cb)
loop until EOS / max_new_tokens / stop()
G->>D: step({cur_token}, pos)
D->>M: execute("forward"|"decode")
M->>K: instructions (KV cache mutable buffer updated in planned memory)
K-->>D: logits
D->>S: sample → cur_token
G->>Tok: decode(prev, cur)
G-->>App: token_cb(text)
end
G-->>R: n_generated
R-->>App: stats_cb(Stats: TTFT, tok/s ...)
Source: text_llm_runner.cpp, text_prefiller.h, text_token_generator.h#L83, text_decoder_runner.cpp#L40, io_manager.h, method.cpp#L1697.
7(b) TTS: Voxtral TTS streaming synthesis (CPU or CUDA) [Verified structure; per-method details Inferred]
sequenceDiagram
participant App
participant TR as VoxtralTtsRunner
participant Tok as Tekken tokenizer
participant LM as model.pte (5 methods: LM + flow head)
participant CD as codec_decoder.pte
participant BE as XNNPACK / CUDA(AOTI) delegate
App->>TR: ctor(model_path, codec_path, tokenizer, voice embedding, [aoti .ptd])
TR->>LM: Module::load_method(...) for LM + flow-head methods
TR->>CD: Module::load_method("forward")
App->>TR: synthesize_streaming(text, AudioChunkCallback)
TR->>Tok: encode(text)
TR->>LM: prefill(text tokens + voice embedding)
loop per audio frame (autoregressive)
TR->>LM: decode step → hidden state
TR->>LM: flow head: 7-step Euler ODE (x0 from xorshift64 rng) → 37 codebook tokens
LM->>BE: DelegateCall (AOTI container run / xnn runtime)
end
Note over TR: after streaming_initial_chunk_ (5 frames) then every streaming_chunk_frames_ (25) with left context 25
TR->>CD: execute(codebook tokens window) → PCM 24 kHz
CD->>BE: DelegateCall
TR-->>App: AudioChunkCallback(float PCM chunk)
TR-->>App: final WAV via wav_writer
Source: voxtral_tts_runner.h, voxtral_tts_runner.cpp, README. Exact method names inside model.pte were not enumerated here [Unknown].
Multimodal VLM path (for VLA-style vision+text) is the MultimodalRunner sequence: vision_encoder(image) → token_embedding(tokens) → text_decoder(embeds, start_pos) prefill → token loop (multimodal_prefiller.cpp).
8. Tests and examples
- Python tests [Verified]:
pytest -n autofrom repo root (CLAUDE.md); pytest.ini ignoresbackends/arm/**(needsexamples/arm/setup.sh), some XNNPACK op tests,backends/test(WIP harness), LLaVA tests, backend-specific Muse Glimmer pipelines. Tests live next to code:exir/tests,exir/backend/test,backends/*/test,extension/llm/export/test,examples/models/llama/tests(incl.test_static_attention.py),runtime/test/test_runtime.py. - C++ tests [Verified]: gtest via CMake
EXECUTORCH_BUILD_TESTS=ONthenctest --output-on-failure; Test.cmake enumeratesruntime/*/test,kernels/test,extension/*/test; test/run_oss_cpp_tests.sh; size tests test/build_size_test.sh and test/size_test.cpp; end-to-end test/end2end. - Backend op-coverage harness: backends/test (
suite,harness, FACTO),multi_method_delegate_test.cpp. - CI [Verified]: ~60 workflows in .github/workflows —
pull.yml,trunk/periodic.yml,nightly.yml,_unittest.yml,_android.yml,apple.yml,cuda.yml,cuda-windows.yml,cuda-perf.yml,rocm.yml,metal.yml,mlx.yml,riscv64.yml,qnn-windows-msvc.yml,test-backend-{arm,coreml,cortex-m,nxp,openvino}.yml,_test_cadence.yml,_xtensa_*.yml,_llm_server.yml,_test_arduino_library.yml, wheel builds for linux/macos/windows/aarch64/cuda. - Example apps / runners [Verified]: LLM: examples/models/llama (
main.cpp,runner/,export_llama.py, eval, Android perf table),examples/models/{qwen3,phi-3-mini,gemma3,gemma4,smollm2,lfm2,deepseek-r1-distill-llama-8B,...}; VLM: llava, llama3_2_vision, gemma3, smolvlm, internvl3, muse_glimmer; audio: whisper, voxtral, voxtral_realtime, parakeet, silero_vad, sortformer, granite_speech, emformer_rnnt, wav2letter; TTS: voxtral_tts, supertonic; vision: mobilenet_v2/v3, resnet, deit, dinov2, efficient_sam, yolo12/26, torchvision_vit, edsr; diffusion: stable_diffusion, stable_diffusion_3_5_large. Platform examples: examples/qualcomm (40+ oss_scripts incl. llama, whisper, t5, gemma4, moshi/mimi, DiT, EfficientSAM), examples/apple/coreml, examples/arm (Ethos-U/VGF/Cortex-M notebooks: smollm2, silero_vad, tinystories, mobilesam), examples/mediatek, examples/samsung, examples/nxp, examples/openvino, examples/vulkan, examples/xnnpack, examples/cuda (ROCm pointwise), examples/arduino, examples/espressif, examples/raspberry_pi/pico2, examples/riscv, examples/zephyr, examples/wasm, examples/llm_pte_finetuning, examples/demo-apps/react-native. - Make targets [Verified]: Makefile + per-example
CMakePresets.json(make whisper-cpu|whisper-cuda|whisper-metal,make silero-vad-cpu,make sortformer-cpu,make supertonic-mlx).
9. TTS relevance
Exists [Verified]:
- Voxtral-4B-TTS end-to-end: export (export_voxtral_tts.py) producing
model.pte(Mistral 4B decoder + 3-layer flow-matching head, 5 methods) andcodec_decoder.pte; backendsportable,xnnpack,cuda,cuda-windows; quant--qlinear 4w|8w|8da4w|8da8w,--qlinear-codec,--qembedding;--streamingchunk metadata; C++ runner withsynthesize_streaming+AudioChunkCallback, xorshift RNG for flow noise, WAV writer. Repo-reported (not reproduced): CUDA 4w RTF 0.31× on RTX 5080 with ~2.6 s time-to-first-audio, 3.4 GB.ptd; FP32 CUDA RTF 51× (codec on portable CPU) (README). - Supertonic 3 on MLX: single dynamic FP16
.ptewith methodsduration_predictor,text_encoder,vector_estimator(5 flow steps),vocoder; one-shot and persistent JSONL server mode; repo-reported RTF ≈ 0.028 on Apple Silicon (README, supertonic_runner.cpp, model/vocoder.py). macOS/Apple-Silicon only. - Neural audio codecs: Mimi (Moshi) tests (examples/models/moshi/mimi/test_mimi.py) and a QNN Mimi decoder runner (qnn_mimi_decoder_runner.cpp); Voxtral codec decoder.
- Audio ops: portable/optimized FFT (
op_fft_r2c/c2rin kernels/optimized/cpu, pocketfft third-party), Conv1d→Conv2d transform (backends/transforms/convert_conv1d_to_conv2d_pass.py) for XNNPACK;ConvTranspose1dhandled in Voxtral codec on CPU/CUDA; mel front end exportable. - Streaming audio output: chunked codec decoding with left context (Voxtral TTS); real-time 80 ms chunk ASR (Voxtral Realtime) shows the runtime can sustain streaming loops; VAD streaming runner.
- Text front end: Supertonic
text_processor.cpp; no general G2P/phonemizer library. - NXP README explicitly lists TTS as a Neutron NPU use-case (backends/nxp/README.md) [Verified statement, no TTS example for NXP found].
Missing / gaps [Verified unless noted]:
- No generic TTS runner abstraction (unlike
extension/llm/runnerorextension/asr/runner); each TTS model has a bespoke C++ runner. - No mobile (Android/iOS/QNN/CoreML) TTS example; Supertonic is MLX-only, Voxtral TTS is CPU/CUDA. [Inferred] the XNNPACK path is fp32 (
--dtype fp32default) and 4B params — heavy for phones. - No vocoder zoo (HiFi-GAN, BigVGAN, WaveRNN, etc.) or codec encoders (EnCodec/SNAC/DAC) in tree;
stable_diffusionhas no audio variant. - Flow-matching ODE loops are unrolled by the runner (7 Euler steps as repeated method calls), not expressed as a graph; no CFG batching helper.
- No audio-specific streaming buffer/jitter management; callbacks are synchronous.
- Sampling for audio tokens (multi-codebook, 37 codebooks per frame) is handled inside the example, not by
Sampler.
10. VLA relevance
Exists [Verified]:
- Vision encoders: CLIP (LLaVA), Llama 3.2 Vision encoder + cross-attention decoder, SigLIP-style Gemma3/SmolVLM/InternVL3 via optimum-executorch converters (examples/models/smolvlm/convert_weights.py), DINOv2, ViT, DeiT, EfficientSAM, YOLO, MobileNet;
MultimodalRunnerhandles image → embeddings → decoder; C++ImageProcessorfor resize/normalize/YUV. - Structured inputs: methods accept arbitrary tensor tuples (
Method::set_input), so proprioceptive vectors are trivially additional inputs [Inferred: no example does this]. - Low-latency loops: static memory planning, no allocation in steady state,
set_output_data_ptrzero-copy outputs, CUDA-graph replay for fixed-shape methods, Vulkan/QNN prepacked graphs,Method::executeoverhead is a flat instruction walk — good fit for fixed-rate control loops. Static attention IO manager demonstrates fixed-pointer I/O for NPUs. - Diffusion/flow machinery: SD/SD3.5 model definitions, DiT on QNN, Voxtral/Supertonic flow-matching heads driven by runner-side Euler loops;
MoveCall/JumpFalseCallallowtorch.cond/whilecontrol flow in a single method (get_while_nodes,get_scan_nodesin memory planning) — [Inferred] a fixed-step denoising loop could be exported astorch.ops.higher_order.while_loopbut no example does so. - Action heads: nothing VLA-specific; an MLP/transformer action head is just another method.
Missing [Verified]:
- No VLA models (OpenVLA, π0, ACT, Diffusion Policy, SmolVLA, Octo), no LeRobot integration, no "robot" mentions in docs/examples/wiki.
- No multi-method orchestration primitive for encoder + policy + action-decoder pipelines beyond writing a C++ runner; no shared-memory/DMA-BUF camera input path (image bytes must be copied into an ExecuTorch tensor).
- No batched multi-camera encoder execution beyond exporting a batched shape.
- Jetson/embedded-CUDA deployment is undocumented (see §11).
[Proposal] For an edge VLA on ExecuTorch: export vision_encoder (per camera, batched), proprio_proj, text/token_embedding, policy_decoder (static KV, SMART_MASK style) and action_head (flow/diffusion step as a method called N times) into one .pte with shared .ptd weights; drive with a small C++ runner modelled on MultimodalPrefiller + VoxtralTtsRunner; pin planned arenas once and reuse Method objects across control ticks.
11. Edge deployment profile
- Binary size [Verified, repo-reported]: docs state the core library is "around 50 kB with no operators/kernels or delegates" (kernel-library-selective-build.md); CI thresholds in pull.yml: stripped
size_test≤ 52,168 B (gcc9) / ≤ 45,000 B (clang); bare-metal Armsize_test≤ 118,000 B (GCC 15.2 baseline 115,868 B) and Zephyr preset ≤ 136,020 B. Size tooling:EXECUTORCH_OPTIMIZE_SIZE(-Os -fno-exceptions -fno-rtti),ET_LOG_ENABLED=0, bloaty (.claude/skills/binary-size/SKILL.md, devtools/size_analysis_tool). - Selective build [Verified]:
EXECUTORCH_SELECT_OPS_YAML|LIST|MODEL,EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD,gen_selected_ops(...)macro; generatesselected_max_kernel_num.h(tools/cmake/Codegen.cmake, examples/selective_build). - Dependencies [Verified]: core needs only C++17 and flatbuffers headers (
third-party/flatbuffers); optional pthreadpool + cpuinfo (EXECUTORCH_BUILD_PTHREADPOOL,EXECUTORCH_BUILD_CPUINFO), XNNPACK, Eigen (optimized BLAS), gflags for runners, flatcc for ETDump, torchao (third-party/ao) for quant kernels, tokenizers submodule for LLM; C++c10headers vendored (check-c10-sync.yml). Python side pins PyTorch via torch_pin.py. - Build system [Verified]: CMake (
CMakeLists.txt, presets in tools/cmake/preset withEXECUTORCH_BUILD_{XNNPACK,VULKAN,QNN,COREML,METAL,MLX,CUDA,ROCM,NEURON,ENN,NXP_NEUTRON,OPENVINO,CADENCE,CORTEX_M,VGF,ARM_BAREMETAL,ARM_ETHOSU_LINUX,WEBGPU,WASM,...},EXECUTORCH_BUILD_EXTENSION_{MODULE,TENSOR,DATA_LOADER,FLAT_TENSOR,LLM,LLM_RUNNER,ASR_RUNNER,IMAGE,TRAINING,APPLE,...},EXECUTORCH_BUILD_KERNELS_{OPTIMIZED,QUANTIZED,LLM}); Buck2 (BUCK/targets.bzl) for Meta-internal; Gradle for AAR;Package.swiftfor SwiftPM; Arduino library builder;pip install executorchwheels for Linux/macOS/Windows (+ CUDA and aarch64-CUDA wheels). - Platform matrix [Verified from backend docs/dirs]:
- Embedded NVIDIA / Jetson: [Unknown] — CUDA backend documented only for "Linux or Windows" x86-class hosts; aarch64 Linux CUDA wheels are built in CI, which [Inferred] would cover Jetson/GB10-class aarch64 if the AOTI
.sois compiled for the target SM, but no doc, example or CI run targets Jetson. - ARM CPU: XNNPACK (ARM64/ARMv7, KleidiAI i8mm/dotprod kernels by default on ARM), torchao low-bit kernels (
torchao:8da4w),riscv64-linuxpreset also exists; Raspberry Pi tutorial (raspberry_pi_llama_tutorial.md). - Cortex-M / MCU:
arm-baremetal,zephyr,esp-baremetalpresets; Cortex-M CMSIS-NN dialect; Ethos-U55/65/85 delegate; Arduino library; Pico2 example; Alif/Zephyr tutorial (zephyr_alif_tutorial.md). - Apple: CoreML (ANE/GPU/CPU, iOS/macOS), MLX (Apple-Silicon GPU, macOS ≥ 14, iOS ≥ 17 experimental), Metal (experimental AOTI), XNNPACK; distributed as xcframeworks
executorch, executorch_llm, backend_coreml, backend_mlx, backend_xnnpack, kernels_llm/optimized/quantized/torchao. - Android: XNNPACK, Vulkan (Adreno/Mali; Maven artifact
executorch-android-vulkan), QNN HTP (Snapdragon; 38 chipset ids incl. automotive SA8295/SA8797), MediaTek Neuron (D9300/D9400), Samsung ENN (Exynos 2500), Arm VGF (Vulkan ML). NNAPI: not present in tree. OpenCL: not present (Vulkan only). - Intel: OpenVINO CPU/iGPU/dGPU/NPU.
- Browser: WebGPU + WASM bindings.
- Embedded NVIDIA / Jetson: [Unknown] — CUDA backend documented only for "Linux or Windows" x86-class hosts; aarch64 Linux CUDA wheels are built in CI, which [Inferred] would cover Jetson/GB10-class aarch64 if the AOTI
- Model conversion constraints [Verified]: must be
torch.export-able (no data-dependent Python control flow excepttorch.cond/while_loop; static or bounded dynamic shapes); one.pteper backend; QNN/Ethos-U/CoreML prefer static shapes and quantized graphs; Ethos-U requires Vela-compilable int8 TOSA; dim-order/channels-last pitfalls documented in .wiki/export/common-pitfalls.md; CUDA export needsEdgeCompileConfig(_check_ir_validity=False, _skip_dim_order=True)(cuda-overview.md); PTE BC guarantee is ≥ one following minor release (runtime/COMPATIBILITY.md). - Repo-reported perf (not reproduced): Llama 3.2 1B SpinQuant on OnePlus 12 (XNNPACK+KleidiAI): 50.2 tok/s decode, 0.3 s TTFT, 1,083 MiB PTE, 1,921 MiB RSS; 3B SpinQuant 19.7 tok/s (llama/README.md#L70); WebGPU Llama 3.2 1B 4-bit on M4 Pro 188 tok/s decode @128 ctx (webgpu README).
12. Limitations and unknowns
- [Verified] Single-method, synchronous, sequential execution; no request-level batching in the core;
extension/llm/batchinghas no productionExecutorimplementation in tree. - [Verified] Global static registries (backends ≤ 16, kernels ≤ 2000 by default) with an explicit TODO that multiple
Executorinstances are not supported in the long term. - [Verified] Backend availability is checked only at delegate init; no runtime fallback if a delegate is unavailable — a
.pteis tied to its backend. - [Verified] KV cache is a static, pre-sized mutable buffer per method; no paging/eviction/prefix sharing in the core (experimental
CellCache/SequenceCacheexist but are wired only through custom ops and engine examples). - [Verified] Dynamic shapes are upper-bound planned;
DYNAMIC_UNBOUNDoutputs need caller-provided memory; several NPU delegates (QNN, Ethos-U) are static-shape only per wiki. - [Verified] No VLA/policy models, no robotics integration, no general TTS runner, no vocoder/codec library; TTS examples are CUDA/MLX-centric.
- [Verified] Tokenizers, XNNPACK, pthreadpool, cpuinfo, MLX, Vulkan headers, flatbuffers, torchao are uninitialized submodules in this checkout — their internals (e.g., tokenizer implementations, XNNPACK version, KleidiAI integration details) could not be inspected.
- [Unknown] Jetson/embedded CUDA support status; whether AOTI-compiled
.soblobs are portable across SM versions and aarch64. - [Unknown] Exact method set and shapes inside Voxtral TTS
model.pte; exact QNN LLM multi-graph memory layout; Metal and Core AI backend maturity. - [Unknown]
examples/llm_serverC++ single-binary server ("future"); Android/iOS native demo apps are referenced by docs but not present in tree (only react-native). - [Inferred] Cross-method sharing of a KV cache (e.g., encoder→decoder cross-attn cache,
update_cross_attn_cacheop) is supported by custom ops but memory planning treats each method independently, so shared state must be threaded as inputs/outputs or held off-graph. - [Inferred] Thread-count control is global (
_unsafe_reset_threadpool), so co-scheduling two models (e.g., VLM + TTS) on CPU has no per-model thread budget.