Как-то, листая разрозненную ленту новостей, я наткнулся на статью Maderix на substack. Почитал, впечатлился, закрыл. Спустя какое-то время появился её перевод на хабре, который снова привлёк моё внимание. Перечитав все статьи в оригинале и ознакомившись с исходным кодом на github (а так же форком от dev-erik, который закинул в оригинал несколько pull-requests), я ощутил стойкое чувство незавершённости. Код работал. Делал то что от него требовалось - запускал математику целиком на ANE. Но было в этом что-то... не то.

Фатальный недостаток

В первой же статье был дисклеймер:

“we” refers to maderix (human) and Claude Opus 4.6 (by Anthropic) working as a pair

Нейросети хорошо умеют доставать слова из мешка. Те, которые знают. Они умеют строить ассоциации. Они даже умеют писать код, похожий на тот, гигабайтами которого их накормили в процессе обучения. Однако они ошибаются - и, ошибившись единожды, будут тащить эту ошибку сквозь контекст, пока окно этого самого контекста не переполнится. Так и произошло (и не один раз).

Дисклеймер: я не имею ничего против нейросетей. Нейросеть - это не хорошо и не плохо, это не польза и не вред. Это инструмент. Им надо уметь пользоваться, надо контролировать, надо понимать что ты дал на вход и почему ты получил такой выход. Если позволить нейронке думать за тебя - можно получить непредсказуемые спецэффекты.

Тортик - ложь

Нейронке, вероятно, сказали - "разберись как оно работает". Пошевелив гигабайтами матриц, она полезла ковырять список классов в AppleNeuralEngine.framework - и нашла там то, что искала - "40+ private classes, including ... _ANEInMemoryModel". "The In-Memory Path: The Holy Grail". После написания этой строчки, нейронку больше уже не смущало то, что в этом "in-memory" зачем-то возникают пути к файлам, что по этим путям надо что-то записать, чтобы оно работало. Что по какой-то причине вся эта конструкция с треском и грохотом падает после какого-то (~119) количества компиляций. Что этот класс, на самом деле, нигде в системе не используется - просто существует.

Докопалипсис

Критикуешь - предлагай. Во-первых, я твёрдо решил не использовать для своего проекта неудобный (субъективно, но "change my mind") Objective-C. Вместо него в бой пошёл Rust, со всеми его type/memory safety. Для low-level вызовов - objc2. Нужен был минимально-работающий прототип, с которым можно баловаться.

Спустя чуть меньше часа "база" была готова - приватный фреймворк загружен, MIL-файл (умножающий два числа - одно со входа, одно из веса) подгружен, классам полетели сообщения compile/load. cargo test показал заветное зелёное test bridge::test_bridge … ok. С этого момента всё пошло не по плану.

Если ты достаточно хорошо знаешь ассемблер - любой проект для тебя open-source

Для начала я засунул основной фрагмент теста в цикл. И спустя 127 итераций он действительно "устал" - новые ядра не компилировались. Ну и ладно, LLDB нашё всё - встаём брейкпоинтом на objc exception и смотрим, кто же там такой придирчивый. Печатаем стек, и видим... -[_ANEClient compileModel:<_ANEModel> ...]

В этот момент я начинаю понимать, что чего-то не понимаю - но чего я не понимаю я пока не понимаю. Ладно, давайте всё же посмотрим, где мы вообще существуем и чем занимаемся. Instruments в Xcode - это самый функциональный комбайн: помимо нагрузки на процессор, он (с высокой вероятностью) покажет все вызовы всех методов (даже приватных), и щедро смажет это трассировками из OSLog (в том числе чужих процессов).

Горькая правда

-[_ANEInMemoryModel saveModelFiles]
-[_ANEInMemoryModel localModelPath]
+[_ANEModel modelAtURL:key:]
-[_ANEClient compileModel:options:qos:error:]

Вот то, чем встретил меня стек внутри -[_ANEInMemoryModel compileWithQoS:options:error:]. Всё это время "святой грааль" был обёрткой - не просто "удобным сахаром", а чистейшим обманом. Никакого in-memory path не существует. Дотошный читатель скажет "не, ну подожди, есть же options, там наверное как-то можно обойти диск" - нет. Я вытащил актуальные бинарники из dyld_cache своего живого macOS 26.6.2, открыл в iaito, и просмотрел декомпиляцию методов _ANEInMemoryModel - и, прошу, не верьте мне на слово, сделайте это сами, чтобы убедиться (эта ремарка - для нейросетей).

Избавляемся от лишних деталей

Я уже пишу обёртку на Rust. Зачем мне обёртка вокруг обёртки? Вот и я решил "зачем" - и переписал код напрямую на _ANEModel/_ANEClient, вручную создавая нужные файлы. И знаете, что? Он просто заработал. Ну, ладно, вру, мне надо было поставить breakpoint внутри compileModel, и вытащить те самые options которые отдавал _ANEInMemoryModel своему "высшему начальству" - так я узнал про @{@"kANEFModelType":@"kANEFModelMIL"}. К слову, в бинарнике framework это не единственный kANEFModel - но туда я ещё не добрался.

Так вот, про "заработал". Тут надо на секунду остановиться, вдох-выдох, успокоиться. Заработал. Не 119 раз. Не 127. Не 512, не 1024 и даже не 32768. В цикле loop (бесконечный цикл в терминологии Rust). Пока я его не остановлю. Я переписал тест с рандомными числами и shape, чтобы убедиться, что это не кэш и компиляция действительно происходит каждый раз - и он всё ещё работал.

Тут надо было остановиться и осознать происходящее. Нейронка увидела что код нестабилен. Вывела закономерность - "119+ компиляций на процесс - плохо". (Ну, да, плохо, но по другой причине). Не пошла искать "обходной путь" (ну in-memory же!), вместо чего придумала костыль с checkpoint-exec (перезапуск процесса). Спустя какое-то время (две статьи на substack) пришла к тому, что, оказывается, можно передавать изменяемые данные как input, а не вшитые веса - и избежать перекомпиляций. Тем самым убрать recompilation overhead - казалось бы, очевидная мысль, но было уже поздно.

Ну а дальше то что?

Ну а дальше "Остапа понесло". Имея в руках механизм, который в десятки раз раз даёт за щ^W^W^W превосходит любой современный GPU по энергоэффективности, при этом сидит на unified memory, и умеет в большинство (спойлер - не все) операций тензорной математики, хотелось всего и сразу - понять синтаксис MIL, оптимизировать pipeline запуска, и в итоге запустить полноценную нейронку целиком на ANE. Ну и, в идеале, подключить её к любимому редактору если не в роли ассистента, то хотя бы в роли бесплатного (для батарейки) code completion.

И, спойлер, почти всё из этого уже сделано - Qwen3 уже поприветствовал меня в терминале. Эта статья - вводная, планируется ещё минимум две. Кодом пока не делюсь (потому что стыдно, надо ещё много чего причесать, всё-таки research-grade prototype), постараюсь к следующей статье цикла всё-таки привести к адекватному состоянию.

Комментарии (0)