Thomas Piacentini
← Workshop

Cheat sheet

Edge Inference Targets

Original text · English

Pick an edge target and a quantization format. Bandwidth ceilings, formats, export steps and on-device measurement.

Four edge targets side by side

FactorApple ANEQualcomm HexagonWebGPU / WebLLMNVIDIA Jetson
Peak38 TOPS, M4 / A1845 TOPS, X Elite / 8 Gen 4Browser GPU, no installOrin Nano Super, AGX Orin, Thor
MemoryUnified, no copySeparate memory domainBrowser penalty ~25%Memory-bound on Nano
AccessCore ML or MPSQNN SDK, AI HubWebGPU compute shadersvLLM, TensorRT Edge-LLM
FormatINT4 weights + FP16 activationsQNN INT8 / INT4Q4 MLC (q4f16_1)Q4 GGUF, TRT-LLM INT4, NVFP4
Observed8 tok/s estimate, A18, 8B Q47 tok/s, 8 Gen 3, 8B Q4~41 tok/s, M3 Max, 8B Q4~40 tok/s, AGX Orin, gpt-oss-20b
LimitNative ANE needs Core MLConvert from PyTorch or ONNX~70-75% mobile coverageNano fits 3B models
Sources

Use each target when

Apple ANE
an iOS app. Convert with Core ML. INT4 weights plus FP16 activations.
Qualcomm Hexagon
Snapdragon phones and Windows ARM laptops. Convert with QNN or AI Hub.
WebGPU / WebLLM
a browser app with no install. Keep a server fallback on the same OpenAI-compatible API.
Jetson Orin Nano Super
Llama 3.2 3B or Phi-3 on 8GB.
Jetson AGX / Thor
larger models, NVFP4 plus FP8 KV, EAGLE-3.
GGUF Q4_K_M
CPU and Apple Silicon laptops.
GPTQ or AWQ INT4
GPU inference with CUDA or ROCm.
Sources

Decode ceiling

def ceiling(target: Target, model_gb: float) -> float:
seconds_per_token = model_gb / target.bandwidth_gb_sall weights per token
return 1 / seconds_per_tokentok/s upper bound
model_gb = 4.7Llama 3.1 8B Q4
Target("Apple A18 (iPhone 16)", 60, 8, "phone with ANE")GB/s, observed tok/s
Target("Snapdragon 8 Gen 3", 77, 7, "mid/high Android")
Target("WebGPU on M3 Max", 400, 41, "browser penalty ~25%")
Target("Jetson AGX Orin", 205, 45, "edge-datacenter bridge")
Sources

Bandwidth math

Rule
decode reads the full set of weights for every token.
7B Q4
3.5 GB. At 50 GB/s one token takes 70 ms, ~14 tok/s.
High-end mobile
90 GB/s gives ~25 tok/s.
HBM3 at 3 TB/s
1.2 ms per token, ceiling 830 tok/s.
Gap
30-50x between datacenter and mobile DRAM.
Long context
4 GB model plus 2 GB KV at 32K on an 8 GB phone is OOM.
Context on edge
keep 4K-8K unless you accept Q4 KV.
Sources

Quantization formats, Llama 3 70B

FactorFP16FP8AWQ INT4GGUF Q4_K_M
Size140GB70GB35GB40GB
Perplexity3.123.144.184.25
MMLU79.5%79.3%78.1%77.9%
Tokens/sec38, A10055, A10075, A10028, CPU
Best forBaselineH100 deploysGPU inferenceCPU, Metal
Sources

Quantize and dequantize

scale = max(abs(tensor)) / 127symmetric INT8
quantized = clamp(round(tensor / scale), -128, 127)
reconstructed = quantized * scaleerror at most scale / 2
quantized = round(tensor / scale) + zero_pointasymmetric, for ReLU outputs
Sources

Tasks

TaskCode
Edge decode ceilings, lesson 12code/main.py
Measure p50, p95, p99 on the devicemeasure_latency(model, input_shape, device="cpu", warmup=10, iters=50)
Time GPU work correctlytorch.cuda.synchronize() before and after the timed block
Static INT8 PTQ configmodel.qconfig = tq.get_default_qconfig(backend)
Export to ONNXtorch.onnx.export(model, sample_input, path, opset_version=17)
GPTQ INT4GPTQConfig(bits=4, group_size=128)
GGUF Q4_K_Mllama.cpp/build/bin/llama-quantize llama-8b-f16.gguf llama-8b-q4km.gguf Q4_K_M
WebLLM weightsmlc_llm convert_weight plus a compiled .wasm
Sources

Vision deployment path

  1. Pick a backbone under 3M params, MobileNetV3-Small. 3-10M, EfficientNet-Lite-B0.
  2. Measure warm up 5-10 passes. Fix the input size to production resolution.
  3. Quantise post-training static INT8. Verify accuracy loss under 1%.
  4. Export PyTorch to ONNX. dynamic_axes allows any batch size.
  5. Runtime TensorRT on Jetson. Core ML on iOS. TFLite on Android.
  6. Decide from a table of model, latency, memory and accuracy measured on the device.
Sources

Troubleshooting

SymptomCauseFix
8B Q4 at 3 tok/s on iPhone 16 ProMobile bandwidth gapSmaller model or fewer bits
OOM at long context on a phoneKV cache for 32K is 2 GBKeep context at 4K-8K or use Q4 KV
WebLLM rejects a GGUF fileGGUF is not supportedConvert to Q4 MLC (q4f16_1)
llama.cpp on iOS skips the ANEMetal backend uses MPSConvert with Core ML
No WebGPU on an old Android browserNeeds Chrome v121+Fall back to the server API
First latency numbers too highCold caches and JITWarm up with 5-10 dummy passes
Naive INT4 output is garbageRounding errors accumulateGPTQ or AWQ with calibration data
Perplexity delta above 2.0Something went wrongRecheck. Under 0.5 is excellent
Sources

Format glossary

BF16
8 exponent bits, 7 mantissa bits. Same range as FP32.
FP8 E4M3
weights and activations for inference.
FP8 E5M2
gradients in training. More range.
INT8
256 values from -128 to 127. Needs a scale factor.
INT4
16 levels. Needs GPTQ or AWQ for quality.
GGUF
llama.cpp file with mixed precision layers.
PTQ
quantize after training. 128-1024 calibration examples.
QAT
fake quantization in training. Better at INT4 and INT2.
Sources

Q&A

Why is compute secondary on edge?
Decode is memory-bound. Bandwidth sets the ceiling.
How fast is WebLLM against native?
About 70-80% of native on the same backend.
FLOPs or latency?
FLOPs for architecture search. On-device latency for deployment.
PTQ or QAT for vision?
PTQ static gives 95% of the benefit with 5% of the effort.
Is FP8 safe?
Yes on H100. 30-50% faster with under 0.1% quality loss.
Sources
Original source
---
id: edge-inference
title: Edge Inference Targets
code: EI
subtitle: Pick an edge target and a quantization format. Bandwidth ceilings, formats, export steps and on-device measurement.
hue: 262
sources: phases/17-infrastructure-and-production/12-edge-inference, phases/04-computer-vision/15-real-time-edge, phases/10-llms-from-scratch/11-quantization
source_hash: a5869cfd5f78f468
---

## Four edge targets side by side
type: comparison
span: 2
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/17-infrastructure-and-production/12-edge-inference/code/main.py

| Factor | Apple ANE | Qualcomm Hexagon | WebGPU / WebLLM | NVIDIA Jetson |
|---|---|---|---|---|
| Peak | 38 TOPS, M4 / A18 | 45 TOPS, X Elite / 8 Gen 4 | Browser GPU, no install | Orin Nano Super, AGX Orin, Thor |
| Memory | Unified, no copy | Separate memory domain | Browser penalty ~25% | Memory-bound on Nano |
| Access | Core ML or MPS | QNN SDK, AI Hub | WebGPU compute shaders | vLLM, TensorRT Edge-LLM |
| Format | INT4 weights + FP16 activations | QNN INT8 / INT4 | Q4 MLC (`q4f16_1`) | Q4 GGUF, TRT-LLM INT4, NVFP4 |
| Observed | 8 tok/s estimate, A18, 8B Q4 | 7 tok/s, 8 Gen 3, 8B Q4 | ~41 tok/s, M3 Max, 8B Q4 | ~40 tok/s, AGX Orin, gpt-oss-20b |
| Limit | Native ANE needs Core ML | Convert from PyTorch or ONNX | ~70-75% mobile coverage | Nano fits 3B models |

## Use each target when
type: concepts
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/10-llms-from-scratch/11-quantization/docs/en.md

- **Apple ANE**: an iOS app. Convert with Core ML. INT4 weights plus FP16 activations.
- **Qualcomm Hexagon**: Snapdragon phones and Windows ARM laptops. Convert with QNN or AI Hub.
- **WebGPU / WebLLM**: a browser app with no install. Keep a server fallback on the same OpenAI-compatible API.
- **Jetson Orin Nano Super**: Llama 3.2 3B or Phi-3 on 8GB.
- **Jetson AGX / Thor**: larger models, NVFP4 plus FP8 KV, EAGLE-3.
- **GGUF Q4_K_M**: CPU and Apple Silicon laptops.
- **GPTQ or AWQ INT4**: GPU inference with CUDA or ROCm.

## Decode ceiling
type: annotated-code
lang: python
from: phases/17-infrastructure-and-production/12-edge-inference/code/main.py

```python
def ceiling(target: Target, model_gb: float) -> float:
    seconds_per_token = model_gb / target.bandwidth_gb_s  # all weights per token
    return 1 / seconds_per_token  # tok/s upper bound
model_gb = 4.7  # Llama 3.1 8B Q4
Target("Apple A18 (iPhone 16)",   60,   8,  "phone with ANE")  # GB/s, observed tok/s
Target("Snapdragon 8 Gen 3",      77,   7,  "mid/high Android")
Target("WebGPU on M3 Max",       400,  41,  "browser penalty ~25%")
Target("Jetson AGX Orin",        205,  45,  "edge-datacenter bridge")
```

## Bandwidth math
type: concepts
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md

- **Rule**: decode reads the full set of weights for every token.
- **7B Q4**: 3.5 GB. At 50 GB/s one token takes 70 ms, ~14 tok/s.
- **High-end mobile**: 90 GB/s gives ~25 tok/s.
- **HBM3 at 3 TB/s**: 1.2 ms per token, ceiling 830 tok/s.
- **Gap**: 30-50x between datacenter and mobile DRAM.
- **Long context**: 4 GB model plus 2 GB KV at 32K on an 8 GB phone is OOM.
- **Context on edge**: keep 4K-8K unless you accept Q4 KV.

## Quantization formats, Llama 3 70B
type: comparison
from: phases/10-llms-from-scratch/11-quantization/docs/en.md

| Factor | FP16 | FP8 | AWQ INT4 | GGUF Q4_K_M |
|---|---|---|---|---|
| Size | 140GB | 70GB | 35GB | 40GB |
| Perplexity | 3.12 | 3.14 | 4.18 | 4.25 |
| MMLU | 79.5% | 79.3% | 78.1% | 77.9% |
| Tokens/sec | 38, A100 | 55, A100 | 75, A100 | 28, CPU |
| Best for | Baseline | H100 deploys | GPU inference | CPU, Metal |

## Quantize and dequantize
type: annotated-code
lang: python
from: phases/10-llms-from-scratch/11-quantization/docs/en.md

```python
scale = max(abs(tensor)) / 127  # symmetric INT8
quantized = clamp(round(tensor / scale), -128, 127)
reconstructed = quantized * scale  # error at most scale / 2
quantized = round(tensor / scale) + zero_point  # asymmetric, for ReLU outputs
```

## Tasks
type: task-table
span: 2
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/15-real-time-edge/code/main.py, phases/10-llms-from-scratch/11-quantization/docs/en.md

| Task | Code |
|---|---|
| Edge decode ceilings, lesson 12 | `code/main.py` |
| Measure p50, p95, p99 on the device | `measure_latency(model, input_shape, device="cpu", warmup=10, iters=50)` |
| Time GPU work correctly | `torch.cuda.synchronize()` before and after the timed block |
| Static INT8 PTQ config | `model.qconfig = tq.get_default_qconfig(backend)` |
| Export to ONNX | `torch.onnx.export(model, sample_input, path, opset_version=17)` |
| GPTQ INT4 | `GPTQConfig(bits=4, group_size=128)` |
| GGUF Q4_K_M | `llama.cpp/build/bin/llama-quantize llama-8b-f16.gguf llama-8b-q4km.gguf Q4_K_M` |
| WebLLM weights | `mlc_llm convert_weight` plus a compiled `.wasm` |

## Vision deployment path
type: flow
from: phases/04-computer-vision/15-real-time-edge/docs/en.md

1. **Pick a backbone**: under 3M params, MobileNetV3-Small. 3-10M, EfficientNet-Lite-B0.
2. **Measure**: warm up 5-10 passes. Fix the input size to production resolution.
3. **Quantise**: post-training static INT8. Verify accuracy loss under 1%.
4. **Export**: PyTorch to ONNX. `dynamic_axes` allows any batch size.
5. **Runtime**: TensorRT on Jetson. Core ML on iOS. TFLite on Android.
6. **Decide**: from a table of model, latency, memory and accuracy measured on the device.

## Troubleshooting
type: troubleshooting
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/10-llms-from-scratch/11-quantization/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| 8B Q4 at 3 tok/s on iPhone 16 Pro | Mobile bandwidth gap | Smaller model or fewer bits |
| OOM at long context on a phone | KV cache for 32K is 2 GB | Keep context at 4K-8K or use Q4 KV |
| WebLLM rejects a GGUF file | GGUF is not supported | Convert to Q4 MLC (`q4f16_1`) |
| llama.cpp on iOS skips the ANE | Metal backend uses MPS | Convert with Core ML |
| No WebGPU on an old Android browser | Needs Chrome v121+ | Fall back to the server API |
| First latency numbers too high | Cold caches and JIT | Warm up with 5-10 dummy passes |
| Naive INT4 output is garbage | Rounding errors accumulate | GPTQ or AWQ with calibration data |
| Perplexity delta above 2.0 | Something went wrong | Recheck. Under 0.5 is excellent |

## Format glossary
type: glossary
from: phases/10-llms-from-scratch/11-quantization/docs/en.md

- **BF16**: 8 exponent bits, 7 mantissa bits. Same range as FP32.
- **FP8 E4M3**: weights and activations for inference.
- **FP8 E5M2**: gradients in training. More range.
- **INT8**: 256 values from -128 to 127. Needs a scale factor.
- **INT4**: 16 levels. Needs GPTQ or AWQ for quality.
- **GGUF**: llama.cpp file with mixed precision layers.
- **PTQ**: quantize after training. 128-1024 calibration examples.
- **QAT**: fake quantization in training. Better at INT4 and INT2.

## Q&A
type: qa
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/10-llms-from-scratch/11-quantization/docs/en.md

- **Why is compute secondary on edge?** Decode is memory-bound. Bandwidth sets the ceiling.
- **How fast is WebLLM against native?** About 70-80% of native on the same backend.
- **FLOPs or latency?** FLOPs for architecture search. On-device latency for deployment.
- **PTQ or QAT for vision?** PTQ static gives 95% of the benefit with 5% of the effort.
- **Is FP8 safe?** Yes on H100. 30-50% faster with under 0.1% quality loss.
Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓