Standalone reproducer for an 8-bit-integer ANE convolution slowdown on Apple M5. Both weights and activations are quantized (W8A8); this is not weight-only INT8 with FP16 convolution. Model I/O is FP16, with quantize/dequantize operations around the convolution.
Related FP16 reproducer: https://gist.github.com/Anemll/39f657dc48b402747bdd96458edd415f
Download all four source/document files into one directory. Requires Apple Silicon macOS, Xcode command-line tools (clang), Python 3.9+, NumPy, and coremltools. Tested with Python 3.9.25, coremltools 9.0, NumPy 2.0.2 on Apple M5 (Mac17,2), macOS 27.0 (26A5425a).
python3.9 -m pip install 'coremltools==9.0' 'numpy==2.0.2'
# Default: compare shared vs per-channel WEIGHT scales
python3.9 profile_int8_dma.py --out run_both
# Shared weight scale only: exact 1 MiB on-boundary
python3.9 profile_int8_dma.py --out run_tensor --scales tensor
# Check reverse measurement order
python3.9 profile_int8_dma.py --out run_reverse --scales both --reverse
# Same byte count, different shape: 4096 x Cin=4032/4096
python3.9 profile_int8_dma.py --out run_cout4096 --cout 4096 --scales bothEvery run needs a fresh output directory. The script writes models, MIL, all timing samples, medians, payload GB/s, and ratios to report.json. Default: 10 warmups + 64 timed evaluations per shape. Avoid competing ANE workloads. If aned changes PID, the run is marked invalid. This uses a private ANE API and may need adaptation on other OS releases.
This is the single-cluster, 16-core lattice. Do not use these dimensions to infer Ultra behavior. A flat result on another chip is a valid outcome, not evidence that the script failed.
INT8 payload per core:
(Cout / 16) * Cin * 1 byte
| Cout | Cin | Weight bytes/core | MiB/core |
|---|---|---|---|
| 8192 | 2016 | 1,032,192 | 0.984375 |
| 8192 | 2048 | 1,048,576 | 1.0 |
| 4096 | 4032 | 1,032,192 | 0.984375 |
| 4096 | 4096 | 1,048,576 | 1.0 |
The shared-weight-scale variant uses one scale for all weights. Separate HWX inspection on this M5 verified sixteen coefficient descriptors of exactly 0x100000 bytes for each on-boundary shape. HWX tools are not required or included in this reproducer; descriptor sizes are prior observations, not dynamically verified by this script.
Per-channel scales add coefficient bytes. For 8192 x 2048, the inspected size was 0x100400 per core: 1 MiB weights + 1 KiB scales. The extra 1 KiB is 512 output-channel scales x 2 bytes, interleaved as sixteen [64-byte scales + 64-KiB weights] blocks. It is not activation data. The HWX programs one combined coefficient range per core, with no separately programmed scale-fetch task; it cannot establish the physical bus transaction count.
Both variants use a shared activation scale (0.001) at input and output. “Per-channel” here refers to weight scales, not activation scales.
Native call medians, 10 warmups + 64 samples:
| Cout | Scale layout | Off Cin / µs | On Cin / µs | On/off |
|---|---|---|---|---|
| 8192 | Shared | 2016 / 342.25 | 2048 / 878.52 | 2.57x |
| 8192 | Per-channel | 2016 / 341.12 | 2048 / 882.27 | 2.59x |
| 4096 | Shared | 4032 / 354.12 | 4096 / 851.60 | 2.40x |
| 4096 | Per-channel | 4032 / 359.62 | 4096 / 826.96 | 2.30x |
The standalone packaged script was rerun successfully: Cout=8192 gave 2.60x shared / 2.52x per-channel; Cout=4096 gave 2.38x / 2.33x.
The slowdown persists with per-channel scales; the extra bytes do not avoid it. Exact total descriptor-size equality alone is therefore too narrow a description of the trigger. These results do not prove a prefetch-wrap mechanism or separate internal DMA streams.
- The host measures elapsed time around
_ANEClient evaluateWithModel, not Core MLpredict(), compilation, or model loading. It includes native call overhead; it is not a hardware-only DMA counter. - GB/s is nominal INT8 weight payload divided by that elapsed time, excluding scales and activation traffic.
- The generator checks that INT8 weight storage and activation quantization survive model conversion. The convolution weight is constant 127 with FP16 scale approximately 0.001/127, matching the earlier FP16 test's approximately 0.001 weight. This is a synthetic timing test, not a numerical-accuracy benchmark.
- Separate M5 HWX inspection confirmed an 8-bit integer convolution between format-conversion tasks. The reverse-engineered decoder labels its operand format code 1 as UINT8, despite signed INT8 in MIL; do not cite this as proof of a signed hardware encoding.
- Running the script alone does not inspect compiler-selected instruction formats on a new chip/OS.
profile_int8_dma.py: W8A8 model generation and CLI.native_utils.py: native build/timing and chip identification helpers.kernel_dma_mib_host.m: original native timing host from the FP16 gist.README.md: instructions and measured results.
See FFN_SPLIT_PREFILL.md for TP1/2/4/8-style output-channel splits and 1/4/16/32-vector measurements. The complete 2048 -> 8192 projection, including all branches and concatenation, was about 2.1–2.5x faster when split. Shared and per-channel weight-scale results are included.
python3.9 profile_int8_tp_prefill.py --out ./tp_prefill_runThis extension also needs tp_host.m and the existing native_utils.py. It does not require HWX tools. These are single-M5 graph splits, not distributed tensor parallelism or a complete gated FFN.