ANE KernelDMA 1 MiB prefetch-notch profiler (M1 Max / M3 Ultra / M4 / M5). eval_us, not wall clock. No ANEC 1MiB DMA-split flag. A developer released a portable profiler that measures Apple Neural Engine KernelDMA prefetch behavior across M1, M3, M4, and M5 chips by timing the private _ANEClient evaluateWithModel call (eval_us) rather than host-dominated Core ML predict() wall-clock time. The tool probes 1 MiB DMA split behavior on single 16-core clusters and dual-cluster Ultra configurations, finding that the stock 16-core 1 MiB pair is off-lattice on Ultra parts and that no ANEC 1 MiB DMA-split flag exists. | | """Portable KernelDMA 1 MiB prefetch-notch profiler M1 / M3 / M4 / M5 . | | | | | | Article: https://eiln.github.io/posts/ane-dma.html | | | | | | Single-cluster 16 ANE cores — default: | | | | | | python3.9 profile kernel dma mib.py --host-only --label m4p | | | python3.9 profile kernel dma mib.py --out ./kernel dma mib m4p --label m4p | | | | | | Dual-cluster Ultra / 32-core lattice M3 Ultra, M1/M2 Ultra : | | | | | | python3.9 profile kernel dma mib.py --host-only --preset dual --label m3u | | | python3.9 profile kernel dma mib.py --out ./kernel dma mib m3u --preset dual --label m3u | | | | | | Wall-clock Core ML predict is host-dominated and invalid. This script | | | times ANEClient evaluateWithModel eval us . Do not dump layers 0-4. | | | """ | | | from future import annotations | | | | | | import argparse | | | import copy | | | import json | | | import os | | | import platform | | | import statistics | | | import subprocess | | | import sys | | | from pathlib import Path | | | | | | try: | | | import numpy as np | | | except ImportError: | | | np = None | | | | | | try: | | | import coremltools as ct | | | from coremltools.converters.mil import Builder as mb | | | from coremltools.converters.mil.mil import types | | | from coremltools.models.utils import compile model | | | except ImportError: | | | ct = None | | | mb = None | | | types = None | | | compile model = None | | | | | | try: | | | from dspark runtime import acquire native lock | | | except ImportError: | | | acquire native lock = None | | | | | | HERE = Path file .resolve .parent | | | HOST M = HERE / 'kernel dma mib host.m' | | | MIB = 1 << 20 | | | LINE = 64 | | | CORES = 16 single ANE cluster | | | DEFAULT COUT = 4096 | | | DEFAULT CINS = 2016, 2048 | | | ARTICLE = 'https://eiln.github.io/posts/ane-dma.html' | | | | | | SERIALIZED SPLIT RELATED = | | | '--enable-global-channel-splitting', | | | '--enable-forced-maximal-bonded-split=true', | | | '--fspatial-split-in-x', | | | '--fkernel-rewind=enabled', | | | '--split-kernel-section=false', | | | '--disable-cache-prefetch-mask=0', | | | '--global-refinement-in-spatial-split=true', | | | '--enable-segment-aware-kernel-section-split=true', | | | '--enable-l2-batch-splitting=true', | | | '--enable-kernel-split-for-multi-palette-lut=true', | | | | | | DROPPED DMA SPLIT CANDIDATES = | | | 'SplitKernelDMA', | | | 'EnableKernelDMASplit', | | | 'MaxKernelDMASize', | | | 'AvoidOneMiBKernelDMA', | | | 'SplitCoeffBuffer', | | | | | | NOT ONE MIB DMA SPLIT = | | | 'SplitKernelSection', | | | 'SpatialSplit', | | | 'EnableSpatialSplitInX', | | | 'EnableKernelSplitForMultiPaletteLUT', | | | 'EnableSegmentAwareKernelSectionSplit', | | | 'EnableGlobalChannelSplitting', | | | 'EnableL2BatchSplitting', | | | 'EnableForcedMaximalBondedSplit', | | | 'DisableCachePrefetchMask', | | | | | | | | | Dual-cluster Ultra: 2x16 cores. Per-core bytes use cores=32 if Cout is | | | striped across both clusters. Stock 16-core 1 MiB pair is then 0.5 MiB | | | and is the wrong probe. Measured 2026-09-11 on M3 Ultra Mac15,14 . | | | DUAL LATTICE = | | | dict name='16c stock 1mib', cores=16, cout=4096, cins= 2016, 2048 , | | | note='single-cluster article pair; Ultra off-lattice 0.5 MiB/core ' , | | | dict name='32c 1mib cin2048', cores=32, cout=8192, cins= 2016, 2048 , | | | note='32-core 1 MiB; Ultra milder 1mib slowdown ~1.61x' , | | | dict name='32c 1mib cin4096', cores=32, cout=4096, cins= 4032, 4096 , | | | note='same 1 MiB/32-core via Cin=4096; Ultra miss' , | | | dict name='32c 2mib cin8192', cores=32, cout=4096, cins= 8128, 8192 , | | | note='32-core 2 MiB; Ultra milder 1mib slowdown ~1.77x' , | | | dict name='32c 2mib cin4096', cores=32, cout=8192, cins= 4032, 4096 , | | | note='same 2 MiB/32-core via Cin=4096; Ultra miss' , | | | | | | | | | | | | def fp16 bytes per core cin, cout, cores=CORES : | | | if cin <= 0 or cout <= 0 or cores <= 0 or cout % cores: | | | raise ValueError 'positive Cin and Cout divisible by core count required' | | | return cout // cores cin 2 | | | | | | | | | def total fp16 bytes cin, cout : | | | return cin cout 2 | | | | | | | | | def on prefetch notch nbytes : | | | if nbytes <= 0: | | | raise ValueError 'empty transfer' | | | k = max 1, int round nbytes / MIB | | | return abs nbytes - k MIB == 0 | | | | | | | | | def classify ratio ratio, median us on=None : | | | """Map on/off eval us ratio to a notch class. Wall-clock ms is invalid.""" | | | if median us on is not None and median us on = 5000: | | | return 'host dominated invalid' | | | if ratio = 2.0: | | | return 'm3 class notch' | | | if ratio = 1.3: | | | return 'milder 1mib slowdown' | | | return 'no notch' | | | | | | | | | def sysctl name : | | | run = subprocess.run 'sysctl', '-n', name , capture output=True, text=True, timeout=5 | | | return run.stdout.strip if run.returncode == 0 else '' | | | | | | | | | def guess chip hw model, brand : | | | brand l = brand or '' .lower | | | model = hw model or '' | | | text = f'{brand l} {model.lower }' | | | for name in 'm5', 'm4', 'm3', 'm2', 'm1' : | | | if name in text: | | | extra = '' | | | for suf in 'ultra', 'max', 'pro' : | | | if suf in brand l: | | | extra = ' ' + suf.title | | | break | | | return 'Apple ' + name.upper + extra .strip | | | if hw model.startswith 'Mac17' : | | | return 'Apple M5-class hw.model ' | | | if hw model.startswith 'Mac16' : | | | return 'Apple M4-class hw.model ' | | | if hw model.startswith 'Mac15' : | | | return 'Apple M3-class hw.model ' | | | if hw model.startswith 'Mac14' : | | | return 'Apple M2-class hw.model ' | | | if hw model.startswith 'Mac13' or hw model.startswith 'MacBookPro18' : | | | return 'Apple M1-class hw.model ' | | | return 'unknown' | | | | | | | | | def guess ane cores chip name : | | | text = chip name or '' .lower | | | if 'ultra' in text: | | | return 32 | | | return 16 | | | | | | | | | def chip info label=None : | | | brand = sysctl 'machdep.cpu.brand string' | | | model = sysctl 'hw.model' | | | guessed = guess chip model, brand | | | cores = guess ane cores guessed | | | return dict | | | hw model=model, | | | brand=brand, | | | platform=platform.platform , | | | guessed chip=guessed, | | | guessed ane cores=cores, | | | dual cluster=cores = 32, | | | label=label or guessed, | | | python=sys.version.split 0 , | | | | | | | | | | | | def dense hits cin, cout, cores=CORES : | | | bpc = fp16 bytes per core cin, cout, cores | | | row = dict | | | cin=cin, cout=cout, cores=cores, | | | bytes per core=bpc, mib per core=bpc / MIB, | | | on prefetch notch=on prefetch notch bpc , | | | total fp16 bytes=total fp16 bytes cin, cout , | | | | | | if cout % 16 == 0: | | | bpc16 = fp16 bytes per core cin, cout, 16 | | | row 'mib per core 16' = bpc16 / MIB | | | row 'on prefetch notch 16' = on prefetch notch bpc16 | | | if cout % 32 == 0: | | | bpc32 = fp16 bytes per core cin, cout, 32 | | | row 'mib per core 32' = bpc32 / MIB | | | row 'on prefetch notch 32' = on prefetch notch bpc32 | | | return row | | | | | | | | | def default preset label=None : | | | chip = chip info label | | | if chip 'dual cluster' or label or '' .lower in 'm3u', 'm1u', 'm2u', 'ultra' : | | | return 'dual' | | | return 'stock' | | | | | | | | | def host report cins=DEFAULT CINS, cout=DEFAULT COUT, label=None, cores=None : | | | chip = chip info label | | | cores = int cores or chip 'guessed ane cores' | | | points = dense hits cin, cout, cores for cin in cins | | | return dict | | | article=ARTICLE, | | | chip=chip, | | | cores=cores, | | | cout=cout, | | | points=points, | | | dual lattice=list DUAL LATTICE , | | | compiler=dict | | | has 1mib kernel dma split flag=False, | | | serialized split related=list SERIALIZED SPLIT RELATED , | | | dropped dma split candidates=list DROPPED DMA SPLIT CANDIDATES , | | | not one mib dma split=list NOT ONE MIB DMA SPLIT , | | | note= 'ANEC split flags cover spatial/batch/channel/section/LUT. ' | | | 'Fabricated KernelDMA 1 MiB keys are dropped by ' | | | 'ANECCreateCompilerOptionsCFString. Workaround is pad/split ' | | | 'the GEMM so each cluster core is not k 1 MiB. Ultra dual-cluster ' | | | 'needs the 32-core lattice; stock 16-core 4096x2048 is 0.5 MiB.' , | | | reference m5 evalus=dict | | | d2016 us=353.8, d2048 us=785.5, ratio=2.22, gbps= 46.7, 21.4 , | | | class name='m3 class notch', cores=16, | | | host='kernel dma mib host.m' , | | | reference m3 article=dict | | | d2016 gbps=44.5, d2048 gbps=16.93, ratio=2.63, | | | class name='m3 class notch', cores=16 , | | | reference m3 ultra 20260911=dict | | | note= 'Mac15,14 2x16 ANE. Do not cite stock 4096x2048 0.5 MiB/core . ' | | | 'Hits: 8192x2048 ~1.61x 66 vs 42 GB/s ; 4096x8192 ~1.77x 89 vs 51 GB/s . ' | | | 'Same byte-count with Cin=4096 missed. M5 Max Mac17,6 flat on dual lattice.' , | | | | | | | | | | | | def build host dest : | | | dest = Path dest | | | run = subprocess.run | | | 'clang', '-O2', '-fobjc-arc', str HOST M , '-o', str dest , | | | '-framework', 'Foundation', '-framework', 'CoreVideo', | | | '-framework', 'IOSurface', | | | '-F/System/Library/PrivateFrameworks', '-framework', 'AppleNeuralEngine' , | | | capture output=True, text=True, timeout=30 | | | if run.returncode: | | | raise RuntimeError run.stderr -4000: | | | return dest | | | | | | | | | def convert conv cin, cout, dest : | | | if ct is None or np is None: | | | raise RuntimeError 'coremltools and numpy required for native compile' | | | dest = Path dest | | | dest.mkdir parents=True, exist ok=True | | | pkg, mlc = dest / 'model.mlpackage', dest / 'model.mlmodelc' | | | weight = np.full cout, cin, 1, 1 , np.float16 0.001 | | | specs = mb.TensorSpec shape= 1, cin, 1, 1 , dtype=types.fp16 | | | | | | def conv main x : | | | y = mb.conv x=x, weight=weight, pad type='valid', strides= 1, 1 | | | return mb.identity x=y, name='y' | | | | | | prog = mb.program input specs=specs, opset version=ct.target.iOS18 conv main | | | pipeline = copy.deepcopy ct.PassPipeline.DEFAULT | | | pipeline.remove passes 'common::fuse conv scale', 'common::fuse conv bias' | | | model = ct.convert | | | prog, convert to='mlprogram', compute precision=ct.precision.FLOAT16, | | | minimum deployment target=ct.target.iOS18, pass pipeline=pipeline, | | | skip model load=True | | | model.save str pkg | | | compile model str pkg , destination path=str mlc | | | return mlc | | | | | | | | | def time mlmodelc host, mlc, warm, timed : | | | env = dict os.environ, ANE IDENTITY='kernel dma mib probe' | | | for name in 'ANE INMEM HWX', 'ANE INPUT SPLIT' : | | | env.pop name, None | | | run = subprocess.run | | | str host , str mlc , str warm , str timed , | | | capture output=True, text=True, timeout=120, env=env | | | if run.returncode: | | | raise RuntimeError run.stderr or run.stdout -4000: | | | line = row for row in run.stdout.splitlines if row.startswith 'DMA JSON ' | | | if not line: | | | raise RuntimeError 'native host printed no DMA JSON' | | | payload = json.loads line -1 9: | | | samples = float x for x in payload 'eval us' | | | return dict eval us samples=samples, median eval us=statistics.median samples | | | | | | | | | def probe anec out : | | | """Re-serialize split-related keys on this machine's ANECompiler.""" | | | out = Path out | | | out.mkdir parents=True, exist ok=True | | | src = HERE / 'anec options probe.m' | | | binary = out / 'anec options probe' | | | flags = { | | | 'SplitKernelSection': False, | | | 'EnableSpatialSplitInX': True, | | | 'EnableKernelSplitForMultiPaletteLUT': True, | | | 'GlobalRefinementInSpatialSplit': True, | | | 'EnableSegmentAwareKernelSectionSplit': True, | | | 'EnableGlobalChannelSplitting': True, | | | 'EnableL2BatchSplitting': True, | | | 'EnableForcedMaximalBondedSplit': True, | | | 'DisableCachePrefetchMask': 0, | | | 'EnableKernelRewind': True, | | | 'SplitKernelDMA': True, | | | 'EnableKernelDMASplit': True, | | | 'MaxKernelDMASize': 1048575, | | | 'AvoidOneMiBKernelDMA': True, | | | 'SplitCoeffBuffer': True, | | | } | | | flags path = out / 'flags.json' | | | flags path.write text json.dumps flags + '\n' | | | if not src.is file : | | | return dict status='skipped no probe source', path=str src | | | build = subprocess.run | | | 'clang', '-O2', '-fobjc-arc', str src , '-o', str binary , | | | '-framework', 'Foundation', | | | '-F/System/Library/PrivateFrameworks', '-framework', 'ANECompiler' , | | | capture output=True, text=True, timeout=30 | | | if build.returncode: | | | return dict status='probe build failed', stderr=build.stderr -2000: | | | run = subprocess.run | | | str binary , str flags path , capture output=True, text=True, timeout=30 | | | serialized = run.stdout or '' + run.stderr or '' | | | has dma split = any | | | token in serialized.lower for token in | | | 'split-kernel-dma', 'kernel-dma-split', 'max-kernel-dma', 'one-mib' | | | return dict | | | status='probed' if run.returncode == 0 else 'probe failed', | | | returncode=run.returncode, | | | serialized=serialized.strip -2000: , | | | fabricated keys dropped=not has dma split, | | | has 1mib kernel dma split flag=bool has dma split , | | | | | | | | | | | | def payload gbps cin, cout, median us : | | | return total fp16 bytes cin, cout / 1e9 / median us / 1e6 | | | | | | | | | def run native out, cins=DEFAULT CINS, cout=DEFAULT COUT, warm=3, timed=12, | | | label=None, cores=None, host=None : | | | out = Path out .resolve | | | if out.exists : | | | raise FileExistsError 'fresh kernel-DMA profile directory required: %s' % out | | | out.mkdir parents=True | | | report = host report cins, cout, label, cores | | | cores = report 'cores' | | | report.update status='preparing', passed=False, metric='native eval us' | | | out / 'report.json' .write text json.dumps report, indent=2 + '\n' | | | lock = None | | | try: | | | if acquire native lock is not None: | | | lock = acquire native lock timeout=1 | | | if host is None: | | | host = build host out / 'kernel dma mib host' | | | measured = | | | for cin in cins: | | | dest = out / 'd%d' % cin | | | print 'DMA NOTCH COMPILE', cin, 'cout', cout, 'cores', cores, flush=True | | | mlc = convert conv cin, cout, dest | | | timed row = time mlmodelc host, mlc, warm, timed | | | gbps = payload gbps cin, cout, timed row 'median eval us' | | | row = dense hits cin, cout, cores | | | row.update gbps weight payload=gbps, timed row | | | measured.append row | | | print 'DMA NOTCH TIME', cin, row 'median eval us' , gbps, flush=True | | | off, on = measured 0 , measured -1 | | | slow = on 'median eval us' / off 'median eval us' | | | class name = classify ratio slow, on 'median eval us' | | | report.update | | | status='passed kernel dma profile', passed=True, points=measured, | | | ratio on over off=slow, | | | ratio 2048 over 2016=slow if set cins = {2016, 2048} else None, | | | class name=class name, | | | m3 class notch=class name == 'm3 class notch', | | | milder 1mib slowdown=class name == 'milder 1mib slowdown' | | | out / 'report.json' .write text json.dumps report, indent=2 + '\n' | | | print 'DMA NOTCH RESULT', report 'status' , slow, class name, flush=True | | | return report | | | except Exception as exc: | | | report.update status='stopped no retry', error='%s: %s' % type exc . name , exc | | | out / 'report.json' .write text json.dumps report, indent=2 + '\n' | | | raise | | | finally: | | | if lock is not None: | | | lock.close | | | | | | | | | def run preset out, preset, warm, timed, label, cores override=None : | | | out = Path out .resolve | | | if out.exists : | | | raise FileExistsError 'fresh kernel-DMA profile directory required: %s' % out | | | out.mkdir parents=True | | | lattices = DUAL LATTICE if preset == 'dual' else DUAL LATTICE 0 , | | | host = build host out / 'kernel dma mib host' | | | summary = | | | for spec in lattices: | | | sub = out / spec 'name' | | | cores = cores override or spec 'cores' | | | try: | | | report = run native | | | sub, spec 'cins' , spec 'cout' , warm, timed, label, cores, host=host | | | summary.append dict | | | name=spec 'name' , note=spec 'note' , cores=cores, | | | cout=spec 'cout' , cins=list spec 'cins' , | | | class name=report.get 'class name' , | | | ratio on over off=report.get 'ratio on over off' , | | | points= dict | | | cin=p 'cin' , median eval us=p.get 'median eval us' , | | | gbps weight payload=p.get 'gbps weight payload' , | | | mib per core=p.get 'mib per core' , | | | on prefetch notch=p.get 'on prefetch notch' | | | for p in report.get 'points', , | | | | | | except Exception as exc: | | | summary.append dict name=spec 'name' , error='%s: %s' % type exc . name , exc | | | blob = dict preset=preset, chip=chip info label , scans=summary | | | out / 'summary.json' .write text json.dumps blob, indent=2 + '\n' | | | print 'DMA NOTCH SUMMARY', json.dumps blob 'scans' , indent=2 , flush=True | | | return blob | | | | | | | | | def main argv=None : | | | parser = argparse.ArgumentParser description= doc | | | parser.add argument '--host-only', action='store true' | | | parser.add argument '--out', type=Path | | | parser.add argument '--probe-anec', action='store true' | | | parser.add argument '--label', help='override chip label, e.g. m1m / m3u / m4 / m5m' | | | parser.add argument '--cout', type=int, default=DEFAULT COUT | | | parser.add argument '--cins', default='2016,2048' | | | parser.add argument '--cores', type=int, default=0, | | | help='ANE cores for MiB/core math 0=auto: 32 if Ultra else 16 ' | | | parser.add argument '--preset', choices= 'stock', 'dual', 'auto' , default='auto', | | | help='stock=16-core article pair; dual=16c+32c lattice; auto=dual on Ultra' | | | parser.add argument '--warm', type=int, default=3 | | | parser.add argument '--timed', type=int, default=12 | | | args = parser.parse args argv | | | cins = tuple int x for x in args.cins.split ',' | | | cores = args.cores or None | | | preset = args.preset | | | if preset == 'auto': | | | preset = default preset args.label | | | if args.host only or args.out is None: | | | report = host report cins, args.cout, args.label, cores | | | report 'preset' = preset | | | print json.dumps report, indent=2 | | | if args.out is None and not args.probe anec: | | | return 0 | | | if args.probe anec: | | | dest = args.out / 'anec probe' if args.out else Path 'anec dma split probe' | | | print json.dumps probe anec dest , indent=2 | | | if args.out is not None and not args.host only: | | | if preset == 'dual': | | | run preset args.out, 'dual', args.warm, args.timed, args.label, cores | | | else: | | | run native args.out, cins, args.cout, args.warm, args.timed, args.label, cores | | | return 0 | | | | | | | | | if name == ' main ': | | | raise SystemExit main |