Artikel

    Wat het echt kost om een lokaal model te vertrouwen

    Als je hebt gekeken naar het draaien van LLM's lokaal op Apple Silicon, ben je vrijwel zeker bij MLX uitgekomen. Dat is de juiste standaard. Het is snel, goed gedocumenteerd, en de community model zoo op HuggingFace maakt het de weg van de minste weerstand. Dit is niet dat bericht.

    Jonah Wilbert23 september 2026Bijgewerkt 29 september 2026
    DelenXFacebookLinkedIn
    Wat het echt kost om een lokaal model te vertrouwen

    Ik bouw een persoonlijke AI-assistent die in plaats van één algemeen model een kleine vloot lokale modellen draait: een snel, goedkoop werkmodel voor klusjes als gestructureerde data uithalen uit rommelige tekst, en een trager, groter model als terugval voor alles wat echte redenering vraagt. Dat ontwerp werkt alleen als de werk-aanroepen goedkoop en snel genoeg zijn om er veel parallel te draaien zonder elke keer een commerciële API te raken. Goedkoop en snel, op een Mac, betekende een vraag stellen die de meeste MLX-gebruikers nooit hoeven te stellen: is Apple's eigen native inference-stack, niet MLX, eigenlijk levensvatbaar hiervoor?

    Die stack heeft twee namen die onbekend klinken als je alleen MLX hebt gebruikt. CoreML is Apple's bestaande framework om models direct op de Neural Engine, GPU of CPU te draaien. Core AI is de nieuwere, nog beta-successor die Apple specifiek bouwt voor dit soort LLM-werk op het apparaat. Geen van beide is waar de meeste lokale-inference-gidsen je naartoe wijzen, en daar is een reden voor: ze zijn moeilijker te gebruiken, de documentatie is smaler, en vooraf was niet duidelijk of een van beide de modelkwaliteit zou volhouden zodra echte verzoeken binnenkwamen.

    Zes weken en drie fases werk later: wat er echt gebeurde, en waarom het proces dat me daarbracht uiteindelijk belangrijker bleek dan het antwoord zelf.

    Gewichten in een vorm brengen die macOS snel kan draaien

    CoreML is het voor de hand liggende startpunt op macOS. Apple's framework compileert modelgewichten naar .mlmodelc-bundels die direct op de Neural Engine, GPU of CPU laden, wat de hardware en firmware toestaan. LFM2.5-350M van LiquidAI was de kandidaat voor de werklaag: klein genoeg dat er meerdere varianten naast een groter terugvalmodel op de schijf pasten, snel genoeg voor veel gelijktijdige aanroepen.

    LFM2.5 omzetten vanuit het HuggingFace-checkpoint naar CoreML betekende het door coreml-llm halen, dat een Apple .mlpackage produceert die op het apparaat wordt gecompileerd tot .mlmodelc. Die compilatiestap is het hele punt. Het is wat CoreML snel maakt bij inference: ahead-of-time compilatie naar hardware-specifieke IR, niet alleen het verpakken van gewichten.

    De conversie leverde niet één artifact op. Twee assen, kwantisering (fp16, int8) en contextvenster (2K tot 32K), leverden een matrix van kandidaten op, want de contextlengte bepaalt de KV-cache-grootte per gelijktijdige aanroep en kwantisering ruilt kwaliteit in voor throughput. Drie configuraties overleefden tot kwaliteitstests:

    • lfm2.5-350m-int8-2k: de snelheidsoptie. ~284 tokens/sec, minimale context, laagste geheugen.
    • lfm2.5-350m-fp16-16k: langere context, ~47 tokens/sec, ruimte voor grotere documenten.
    • lfm2.5-1.2b-fp16-32k: de terugval. Bijna 4x de parameters, ~3.2 tokens/sec, de kandidaat voor taken die de 350M-modellen niet aankonden.

    Een ding kwam boven tijdens de conversie dat alles daarna vormgaf: LFM2.5's architectuur heeft een rollende convolutiestaat, conv_state_in / conv_state_out, apart bijgehouden van de standaard KV-cache. Een gewone transformer heeft alleen een KV-staat tussen decode-stappen nodig. LFM2 heeft beide nodig. Als het staatbeheer fout gaat, breekt de output niet zichtbaar. Het degradeert gewoon stil. Dat is een slechter falingsmode dan een crash, want niets vertelt je dat het gebeurd is.

    De architectuurvraag beantwoorden voordat er één regel bridge-code bestaat

    Voordat er serving-code geschreven werd, was er een eerdere vraag die het waard was om goed te settled: hoe delen gelijktijdige aanroepen een geladen model?

    De naïeve aanpak, één MLModel per aanroep, is makkelijk te begrijpen en duur in de praktijk. Een MLModel vanaf schijf laden kost 250MB tot 2.300MB, afhankelijk van de configuratie, en die kosten komen bij elke load. Tien gelijktijdige aanroepen betekent tien loads, wat op een MacBook met 32GB unified memory een echte beperking is, geen theoretische.

    Het juiste patroon, bevestigd over 16 benchmarkconfiguraties in een eerdere testharness, is om één MLModel één keer te loaden en per aanroep een verse MLState te alloceren. Die staat houdt de KV-cache en conv-staat van één sequentie vast: goedkoop (24-385MB) en volledig geïsoleerd. De gewichten dupliceren nooit. Dit is Apple's bedoeldde architectuur, maar niet iets wat je alleen uit de API-docs zou afleiden; de benchmark is wat "waarschijnlijk" in "bevestigd" veranderde. De concurrency-winsten waren echt maar bescheiden, waarbij de 350M int8-2K-configuratie ongeveer 1.16x aggregate throughput haalde bij 4 gelijktijdige aanroepen, wat meer telde voor capaciteitsplanning dan voor het ja/nee over de architectuur zelf.

    Dezelfde benchmark vond ook een omgevingsbug die bleef terugkomen: macOS 26 Tahoe's ANE-firmware faalt stil op .all compute-units en valt door naar GPU of CPU zonder het te zeggen. De fix zit niet in de code. Het is een fallback-keten: vraag .all, vang de fout, probeer .cpuAndGPU, dan .cpuOnly. Wanneer Apple de firmwarefix levert, hoeft er niets te veranderen; .all begint gewoon te werken.

    Iets bouwen dat schoon opstart

    De bridge (de dienst die een aanroep aanneemt en naar het juiste model routeert) is een Swift-binary op Vapor, gestructureerd rond twee actors. ModelRegistry houdt één ModelWorker per model-ID vast en pre-load alles wat in config.json staat bij het booten. Als een model niet laadt, stopt het proces direct, want een luide fout bij startup verslaat een mysterieuze drie aanroepen later in productie. ModelWorker houdt het geladen model vast en allocert per aanroep een verse staat, draait prefill, decodeert, en gooit de staat daarna weg. Geen leakage tussen aanroepen, wat specifiek telt omdat LFM2's rollende conv-staat anders de context van de ene beller meeneemt in het antwoord van de ander.

    Schoon opstarten kostte één echte debugrondes. Het 1.2B FP16-model triggerte een OOM-crash tijdens GPU-compilatie bij startup. De driver probeerde een executieplan voor een 2.4GB-model in GPU-geheugen te bouwen, liep tegen een limiet aan, en macOS gaf SIGKILL. De log stopte gewoon:

    No outputs specified. @ Impl
    

    De fix was het 1.2B-model vastzetten op .cpuOnly in de config. Trager, maar het laadt. Twee kleinere problemen werden in dezelfde ronde gefikst: force-unwraps op CoreML-outputwaarden (veilig tot een model een onverwachte vorm teruggeeft, dan een crash) werden guarded unwraps met echte fouten, en de bridge wees te lange prompts nu direct af in plaats van stille ongeldige KV-cache-posities door te geven aan het model. Daarna loadden en warmden alle drie de modellen het bij de eerste poging:

    [lfm2.5-350m-int8-2k] Loaded successfully and warmed up with computeUnits: MLComputeUnits(rawValue: 1)
    [lfm2.5-350m-fp16-16k] Loaded successfully and warmed up with computeUnits: MLComputeUnits(rawValue: 0)
    [lfm2.5-1.2b-fp16-32k] Loaded successfully and warmed up with computeUnits: MLComputeUnits(rawValue: 0)
    [Boot] CoreML Bridge HTTP server starting on http://127.0.0.1:8090
    

    De vraag die er echt toe deed: kan het het werk doen

    Een model dat opstart en aanroepen served heeft nog niets bewezen. De echte test was een set van 13 taken over vijf categorieën, gebouwd om direct te mappen op wat een werkmodel als dit echt moet doen: extractie, classificatie, retrieval-QA, verificatie en codereview. Elke taak is retrieval-grounded, wat betekent dat het antwoord in de gegeven context staat en nergens anders. Dat is geen ontwerpvoorkeur. Een werker die uit parametrisch geheugen antwoordt wanneer de passage het antwoord niet bevat, is een hallucinatierisico in productie, geen kwaliteitsdetail om je schouders over op te halen.

    Alle drie de modellen draaiden de volledige set bij temperature 0.0, 39 aanroepen totaal:

    TaakCategorie350m-int8-2k350m-fp16-16k1.2b-fp16-32k
    A1ExtractiePASSPASSPASS
    A2Extractie (afleider)FAILFAILPASS
    A3Extractie (HTML)PASSPASSPASS
    B1ClassificatiePASSPASSPASS
    B2Classificatie (adversariaal)FAILFAILPASS
    C1Retrieval-QAFAILFAILPASS
    C2Hallucinatie-onthoudingPASSPASSPASS
    C3Multi-fact-syntheseFAILFAILPASS
    D1Verificatie (contradictie)FAILFAILFAIL
    D2Verificatie (scope-overschrijding)FAILFAILFAIL
    E1Codereview (crashbug)FAILFAILPASS
    E2Codereview (stille bug)FAILFAILFAIL
    E3Codereview (geen-bug-check)PASSPASSPASS
    Totaal5/135/1310/13

    Beide 350M-varianten landden precies op 5/13, met dezelfde taken gefaald om dezelfde redenen, wat op zich de bevinding is. Kwantisering en contextvenster maakten geen verschil voor het falingspatroon, wat int8-precisieverlies uitsluit als boosdoener. D1 en D2 vereisen een bewering en een bron tegelijk vasthouden in aandacht en redeneren over de relatie ertussen, wat inference is in plaats van retrieval, en bij 350M parameters hield die kloof stand over beide varianten. C1 was de interessantere fout: een directe vraag met het antwoord duidelijk in de passage, en het model faalde toch. Dat wees op een prompt-attractor-effect, waar de prior van het model voor hoe een vraag te beantwoorden streed met de instructie om het antwoord uit de gegeven tekst te halen. De prompt herformuleren om harder op de passage te ankeren verbeterde C1 soms, maar niet consistent genoeg om het oordeel te veranderen. E2 toonde een heel ander patroon: het model merkte de juiste functie (een met een legitieme empty-list-guard) op als verdacht, precies de neiging tot vals-positief die die taak moest vangen.

    De bridge uitsluiten voordat je het model de schuld geeft

    Voordat ik iets concludeerde over het plafond van het 350M-model, was er een zorgvuldiger vraag: is dit enigszins de schuld van de bridge? HTTP-routing, chat-template-rendering, input-validatie, token-decoderen door CoreML's Swift-API. Elke laag in die stack kan in principe de kwaliteit degraderen ten opzichte van wat de ruwe gewichten aankunnen. De infrastructuur uitsluiten voordat je het model de schuld geeft is de enige methodologisch eerlijke volgorde.

    Hier verdiende MLX zijn keep, als ground truth om tegen te testen in plaats van het ding dat getest werd. De isolatietest loadde identieke gewichten via mlx_lm, LiquidAI/LFM2.5-350M-MLX-8bit direct van HuggingFace, en draaide het natief via MLX. Geen bridge, geen HTTP-laag, geen Swift CoreML-API ertussen. Zelfde 13 taken, zelfde prompts, zelfde grading, temperature 0.0.

    Resultaat: 5/13. Zelfde taken gefaald. Zelfde taken geslaagd.

    Dat regelt het schoon. Het 350M LFM2.5-model heeft een echt capability-plafond op deze schaal, en het verschijnt identiek of je het door een Swift HTTP-bridge draait of MLX's Python-runtime met ongewijzigde HuggingFace-gewichten. De bridge degradeert niets. Het model is het model. En de 10/13 van het 1.2B-model bewijst dat de set zelf oplosbaar is; niets in het testontwerp vraagt capability die nergens in de LFM2.5-familie bestaat, het bestaat gewoon nog niet bij 350M parameters voor redeneren en meervoudige synthese. Extractie en classificatie handleden de 350M-modellen goed. Hallucinatie-onthouding, de enige eigenschap waar dit hele ontwerp voor veiligheid echt op leunde, hield stand over elke configuratie.

    350M is onbetrouwbaar voor redeneren. 1.2B is levensvatbaar. De runtime-keuze verandert geen van beide oordelen.

    Een vijfstaps-protocol, geen eenmalig antwoord

    Niets hiervan ging echt over LFM2.5. Het is een herbruikbaar protocol om elk lokaal model onder elke runtime te evalueren, en het is de reden dat deze oefening het waard was om zorgvuldig te doen in plaats van snel.

    Pin eerst de gewichtsherkomst: exact checkpoint, exacte kwantisering, exacte conversiepipeline. "350M int8" is op zich niet specifiek genoeg; contextvenster en conversiegereedschap veranderen beide het gedrag. Stel ten tweede een runtime-isolatiebaseline op, door ruwe gewichten door een directe runtime als MLX te draaien voordat je iets test dat een servinglaag aanraakt. Zonder die baseline is een fout via de bridge niet toeschrijfbaar. Draai ten derde de pass/fail-kwaliteitsset. De 13-takstructuur is direct herbruikbaar tegen elke kandidaat, en de hallucinatie-onthoudingstest blijft non-negotiable, hoe goed een model elders ook scoort. Vergelijk ten vierde de resultaten van de bridge met de isolatiebaseline. Een match betekent dat de servinglaag schoon is; een divergentie vernauwt de zoektocht precies tot wat de bridge toevoegt. En voor iets dat net onder de drempel landt, draai ten vijfde een prompt-gevoeligheidsablatie. Het C1-resultaat bewees dat formulering individuele taakuitkomsten verandert terwijl de onderliggende capability vastligt, wat een modeloordeel niet verandert maar enorm telt voor wat er in productie gaat.

    De hele set draait in minuten. De isolatietest kost een uur, grotendeels wachten op een model-download. Dat is een kleine prijs voor een schoon, goed gekarakteriseerd capability-oordeel, en een veel kleinere prijs dan een werker shipsen die stil faalt op redeneertaken zodra hij live is.

    Toen Apple een beta leverde, en het framework zich mocht bewijzen

    Het oorspronkelijke plan zette Core AI, Apple's .aimodel-formaat en de bedoeldde opvolger van CoreML's .mlpackage, onder "later testen". Later kwam dezelfde dag dat macOS 27 Golden Gate beta 4 uitkwam, met een expliciete changelog-regel voor bug 176210080: een fix voor ANE-faalt op bepaalde gewichtsconfiguraties, precies de faalklasse die de 16-passembenchmark al weken documenteerde. De upgrade gebeurde direct.

    De ANEF -14-fout waaraan de bridge workaroundde verdween niet, want de fix richtte zich specifiek op .aimodel-modellen, en de bridge draait .mlpackage, een ander formaat op een compleet ander runtime-pad. "Core AI fixte de ANE-bug" bleek waar én hier niet van toepassing. Eén echte verbetering verscheen wel: .cpuAndGPU loadt en warmt nu schoon voor de kleine modellen, waar het voorheen een Metal OOM op macOS 26 triggerte. GPU-executie werd beschikbaar voor .mlpackage-modellen waar dat eerder niet kon, maar het is nog steeds geen ANE, en nog steeds niet het formaat waar de fix zich op richtte.

    Core AI goed testen betekende het op eigen voorwaarden testen. apple/coreai-models, Apple's eigen exportrecepten, Python-primitives, Swift-runtime en CLI-tools, schoon gebouwd tegen de beta 4-SDK. Qwen3-0.6B exporteren naar .aimodel kostte één commando:

    uv run coreai.llm.export Qwen/Qwen3-0.6B --output-dir /tmp/qwen3-0.6b-int4
    

    Het benchmarkresultaat: 1.426 tokens/sec promptverwerking, 159.5 tokens/sec generatie. Dat is de eerste end-to-end Core AI-inference op deze machine, en 159 tok/s voor een 0.6B-model is snel, ongeveer 19x sneller dan de CPU/BNNS-pad waar de CoreML-bridge doorheen draait. powermetrics toonde het op GPU landen, niet ANE: 6.261 mW piek, nul ANE-watts, en de exportdirectory letterlijk gpu-pipelined genoemd. De compute-plaatsingsbeslissing wordt bij export genomen door Apple's eigen pipeline, en voor macOS vandaag kiest die pipeline GPU voor LLM-generatie. De ANE-fix in beta 4 verwijderde een blocker voor ANE-gerichte modellen. Het veranderde niet waar Apple's eigen scheduler beslist een LLM op een M2 te draaien.

    De hardere test was LFM2.5-8B-A1B, al op schijf in Core AI-formaat via een community-conversie. Apple's eigen llm-runner wees het direct af:

    Error: Invalid output type for 'Expected 2 states (KV cache), got 3:
    ["keyCache", "valueCache", "convState"]'
    

    Apple's toolkit gaat uit van twee KV-staten. LFM2.5 heeft drie: dezelfde rollende conv-staat die al bij de CoreML-conversie telde zit er nog, en het is nog steeds niet iets wat een standaard transformer-runner weet te alloceren. Geen bug. Een gat in wat Apple's catalogus nu ondersteunt. Een community coreai-kit heeft een custom LFM2-engine die de drie-staat-architectuur wel aankan, maar het tegen beta 4 bouwen betekende drie aparte API-breaks patchen: een get-only property die ooit mutable was, een type dat noncopyable werd en expliciete ownership-semantiek nodig had, en ten slotte een runtime-crash bij het resolveren van tensor-layout voor de MoE-routingtensors:

    CoreAIRuntime/NDArray+Layout.swift:126: Fatal error:
    Cannot make a Tensor.Layout from an unresolved TensorRequirements
    

    Dat laatste zit in Apple's eigen CoreAIRuntime, niet in de community-kit-code. Het beta's API-oppervlak bewoog sneller dan de kit bijhield. Twee onafhankelijke paden, twee onafhankelijke oordelen, allebei op dezelfde plek landend: LFM2.5 is op beta 4 nog niet draaibaar op Core AI, of de muur nu architecturaal is (Apple's toolkit) of een bewegend beta-doel (de community-kit). MLX, met first-party LiquidAI-gewichten, een mature generatieloop en geen drie-staat-gat om omheen te werken, blijft voorlopig de correcte runtime voor LFM2.5.

    Wat Core AI echt is, nu het gebruikt is in plaats van over gelezen

    De exportpipeline werkt schoon. De benchmarktooling is solide. Generatieve throughput op GPU verslaat de CoreML/BNNS-pad ruim voor modellen die het toolkit al ondersteunt. Qwen3, Gemma3, Mistral en Mixtral converteren en draaien allemaal zonder incident, en de repository wordt actief onderhouden onder een echte open-source-licentie. Dat is de echte upside, en geen kleine.

    Het nuance is dat Core AI vandaag GPU-first is voor LLM's, niet ANE-first, ongeacht wat de beta 4-changelog zou suggereren voor wie oppervlakkig leest. Het stroomverhaal dat ANE in de eerste plaats het interessante doelwit maakte, verschijnt nog niet voor generatieve modellen zoals het dat doet voor kleinere, niet-generatieve, en of dat in een toekomstige beta verandert is Apple's keuze, niet iets wat een gewichtsformaat-fix beslist. De beperking die er hier echt toe doet: elke architectuur die geen gewone twee-staat-KV-transformer is, heeft expliciete engine-ondersteuning nodig voordat het überhaupt draait, en LFM2.5's hybride conv-attention-ontwerp met zijn derde statetensor valt buiten die ondersteuning. De community-modelzoo bouwt eraan toe. Het jaagt een beta na die blijft bewegen.

    Waar dit dingen laat, en wat er komt

    De bridge werkt. Het served drie modellen vanuit één proces over een schone OpenAI-compatibele API, de foutafhandeling is expliciet in plaats van mysterieus, en de kwaliteit die het levert is precies wat de ruwe gewichten aankunnen, niet meer en niet minder. Voor 350M-schaalmodellen specifiek: betrouwbaar voor extractie en classificatie, nog niet betrouwbaar voor redeneren of meervoudige synthese, en de routinglogica moet dat plafond weerspiegelen in plaats van eromheen te hopen. Voor Core AI: echt, snel, en vandaag klaar voor standaard transformer-architecturen, met een duidelijk migratiepad zodra het nodig is, maar nog geen optie voor LFM2.5-familiewerkers tot Apple engine-ondersteuning toevoegt of de community-kit stabiliseert tegen een beta die stopt met bewegen.

    Het deel dat het waard is om te bewaren, ongeacht welk model wint, is dat het testprotocol niet uitmaakt wat het evalueert. Gewichtsherkomst, runtime-isolatie, de pass/fail-set, bridge-vergelijking, prompt-gevoeligheidsablatie: dezelfde vijf stappen, in dezelfde volgorde, leveren een schoon oordeel of het onderwerp nu een 350M CoreML-werker is of wat Core AI's LFM2.5-ondersteuning over zes maanden levert (inclusief de rest van de lokale modellenvloot die ik er nu tegen draai: planner-, coder- en reviewer-kandidaten, een verhaal voor een andere post). Dat is de eigenschap waar het bij infrastructuurtests in het algemeen om draait: niet een framework dat op een bepaald antwoord hoopt, maar een die je vertelt wat waar is en doorpakt.

    Onderwerpen

    local-inference

    Wil je dit voor je bedrijf laten doen?

    Ik richt praktische AI in, bouw websites die gevonden worden en ontwerp merken die kloppen. Vaste prijzen, scope op schrift, één aanspreekpunt.

    Vaste prijzen, excl. btw · Alles is van jou · Antwoord binnen 24 uur

    Deel dit artikel

    DelenXFacebookLinkedIn
    Plan een gratis gesprek