В базе открыта внешняя обработка. Я пишу в поле "что такое регистр накопления", жму кнопку и жду двенадцать секунд. Она отвечает: "регистр накопления хранит движения и итоги".
Ни интернета, ни внешней компоненты, ни ONNX (это формат, в котором обученные модели отдают наружу для запуска), ни HTTP-запроса, ни Native API. Один файл на 41 килобайт, внутри чистый BSL - встроенный язык 1С:Предприятия, тот самый, на котором вы пишете каждый день: Массив, Соответствие, БуферДвоичныхДанных и циклы. Языковая модель (по-английски large language model, отсюда и LLM) посчитана прямо в 1С, на том же языке, на котором вы пишете проведение документа.

Полный ответ модели при температуре 0: 43 символа, 1 385 344 умножения, 12,6 секунды
Сразу скажу, чем это не является. Это не инструмент. Модель знает сорок фраз про объекты 1С и больше ничего, знаний о мире в ней нет физически. Пользы от неё ноль.
Смысл в другом. Все мы за два года привыкли к словам "токен", "эмбеддинг", "температура", "галлюцинация", и почти никто не видел, как это устроено внутри. Статьи про трансформеры написаны матрицами и через час забываются. А тут можно открыть Конфигуратор, поставить точку останова в середине языковой модели и посмотреть на числа своими глазами. Это второй раз, когда я считаю на голом BSL то, для чего обычно берут внешнюю компоненту; первым был RSA без COM и .NET, но там смысл был прикладной, а здесь объяснительный.
Главный тезис, ради которого всё затевалось, скажу сразу. Внутри модели нет ни одного Если, разбирающего язык. Нет словаря смыслов, нет грамматики, нет правил. Есть таблица чисел, три вложенных цикла умножения и массив активаций. Всё "понимание" - это статистика, застывшая в весах.
Что она отвечает и сколько это стоит
Два прогона рядом, цифры с экрана, не из головы. Первый вопрос модель на обучении видела, второго не видела никогда.
| Вопрос | Ответ | Символов | Умножений | Время |
|---|---|---|---|---|
| что такое регистр накопления | регистр накопления хранит движения и итоги. | 43 | 1 385 344 | 12,6 с |
| как жарить котлеты | документ права документ? | 24 | 876 800 | 7,2 с |
Скорость выходит около 110-122 тысяч умножений в секунду. Запомните этот порядок, он ещё пригодится.
Вторая строка тут главная. Про котлеты в обучении не было ни слова. Модель не сказала "не знаю" и не запнулась: выдала бессмыслицу тем же ровным тоном, каким секунду назад отвечала про регистр накопления, и потратила на неё ровно столько же времени на символ. Уверенность языковой модели никак не связана со знанием. Это и есть механика галлюцинации, показанная на двадцати двух тысячах параметров, и ниже я разберу, почему иначе не бывает.
Что стоит за модными словами
Пойдём по пути, который проходит ваш текст внутри модели. Восемь понятий по порядку: токен, словарь, вектор символа, вес, слой, голова внимания, контекст и вероятности. Дальше отдельными разделами температура, обучение, галлюцинация и KV-кэш. На каждом шаге снимается один слой мистики.
Токен: текст режется на кусочки, кусочкам присваиваются номера
Модель не работает с буквами. Первое, что происходит с вашим вопросом, - его режут на кусочки и каждому кусочку сопоставляют номер через обычное Соответствие. Кусочек и называется токеном. В больших моделях токен обычно кусок слова: "регистр" может быть одним токеном, "сведений" двумя. У меня токен это один символ - так нагляднее, и весь словарь помещается в строку экрана.
Вот весь токенизатор целиком:
Функция ВТокены(Текст) Токены = Новый Массив; Строчный = НРег(Текст); Для Поз = 1 По СтрДлина(Строчный) Цикл Токен = Модель.КодыТокенов.Получить(Сред(Строчный, Поз, 1)); Если Токен <> Неопределено Тогда Токены.Добавить(Токен); КонецЕсли; КонецЦикла; Возврат Токены; КонецФункции
Восемь строк. Никакой лингвистики, обычный Соответствие из символа в число. Вот здесь буквы и заканчиваются: дальше по коду существуют только числа, и вернуть их обратно в буквы можно будет ровно один раз, в самом конце.
Словарь: закрытый список символов, всё остальное не существует
Словарь (его же называют алфавитом) - это закрытый перечень того, что модель вообще различает. У меня их 36: тридцать одна строчная русская буква, пробел, точка, двоеточие, знак вопроса и перевод строки.
Обратите внимание, чего там нет. Нет заглавных букв, нет запятой, нет ё и э, нет латиницы и цифр. Токенизатор приводит вход к нижнему регистру, а всё незнакомое молча выбрасывает (это строка Если Токен <> Неопределено выше). Напишете "Регистр Накопления, 2026" - модель увидит "регистр накопления ".
В обработке всё это показывает кнопка "Что внутри": весь алфавит одной строкой, ваша фраза, разобранная в номера, и устройство модели цифрами. Вот как выглядит фраза "что такое регистр накопления" после токенизатора - двадцать восемь номеров, и ни одной буквы:

Кнопка "Что внутри": алфавит из 36 символов, разбор фразы в номера токенов и устройство модели
На том же кадре внизу видно устройство: 21 920 параметров, 2 слоя, 2 головы внимания, размерность вектора 32, контекст 96 символов, 17 536 умножений на символ. Все эти числа дальше по тексту разбираются по одному, а в конце статьи мы их перемножим и сверим с показаниями счётчика.
Здесь же прячется первое разочарование от больших моделей. Когда GPT ошибается в подсчёте букв в слове, дело в том же самом устройстве, хотя словарь у неё другой: слово для модели это один-два номера из словаря, и отдельных букв внутри номера она не видит. Примерно как вы не видите отдельных байтов, работая с ДвоичныеДанные.
Вектор символа: смысл - это координаты точки
Номер токена сам по себе бесполезен: то, что "а" это 5, а "б" это 6, не значит, что они похожи. Поэтому каждому номеру сопоставлен массив чисел фиксированной длины, у меня из 32 штук. Этот массив и называется вектором символа, или эмбеддингом: для модели смысл символа - координаты точки в тридцатидвумерном пространстве, и больше ничего. В коде это Массив из 36 массивов по 32 числа:
М.Вставить("ЭмбТокенов", ПрочитатьТензор(Буф, Курсор)); // 36 строк по 32 числа
Дальше работает простая идея: то, что встречается в похожих местах, при обучении заезжает в близкие точки. Похожими считаются те векторы, которые смотрят примерно в одну сторону, и меряется это одним числом (как именно, разберу ниже, в разделе про внимание). В больших моделях так рядом оказываются "кот" и "кошка". Никто этого не программировал, координаты подобрались сами.
Вес и параметр: число в таблице, подобранное перебором
Параметр - это одно число в одной из таблиц модели. Их же называют весами. Больше в модели ничего нет: только числа и порядок, в котором на них умножают.
У моей модели параметров 21 920: 1 152 на векторы символов, 3 072 на векторы позиций, 16 384 на два слоя, 160 на нормализацию и 1 152 на выходную таблицу. У GPT-класса этих чисел сотни миллиардов.
Скажу сразу, где кончается сходство, чтобы потом не ловили. Позиции у больших моделей считаются формулой поворота координат; у меня это таблица на 96 строк. Нелинейность там гладкая, у меня грубое обнуление отрицательных. Токен там кусок слова, у меня символ. Отличий наберётся ещё десяток, и в мою модель они сознательно не заложены: с ними код перестаёт читаться. Одинаковой остаётся цепочка: номера, векторы, внимание, полносвязный слой, вероятности, выбор. Дальше в статье разбирается именно она. Устройство конкретно GPT я вам не покажу, его никто снаружи не видел.
Ещё одна честная деталь про веса. В файле каждый из них ужат до одного байта, а настоящее число получается умножением на масштаб строки. Это называется квантованием, и на такой модели оно ответы не портит - проверено сравнением с полной точностью до сборки обработки.
Откуда берутся значения весов - отдельный разговор, он ниже, в разделе про обучение. Пока достаточно знать, что это просто числа в макете внутри обработки.
Слой и голова внимания: на что модель смотрит назад
Слой - это один повторяющийся блок обработки, у меня их два, у больших моделей десятки. Вектор входит, внутри с ним происходят одни и те же операции, на выходе получается другой вектор той же длины, и он идёт в следующий слой.
Внутри слоя живёт внимание. За страшным словом стоит вот что: для текущего символа считается, насколько он интересуется каждым из предыдущих. Получается набор чисел, по одному на каждый прошлый символ, в сумме дающих единицу. Дальше векторы прошлых символов складываются с этими числами как с коэффициентами.
То есть модель, дописывая ответ, смешивает то, что уже прочитала, в разных пропорциях. Пропорции она считает сама на каждом шаге.
Голова внимания - это одна такая процедура, работающая на своём куске вектора. У меня две головы по 16 координат. Разделение сделано для того, чтобы головы могли обучиться разным связям и не мешали друг другу; потом их результаты склеиваются обратно в один вектор.
Теперь про три слова, на которых обычно и ломается объяснение: запрос, ключ и значение. В коде они называются КвО, КаО и ВэО.
Проще всего думать про них как про поиск в справочнике. Из вектора текущего символа тремя разными умножениями получаются три новых вектора:
- запрос - что этот символ сейчас ищет;
- ключ - по чему этот символ можно найти, его "наименование" для поиска;
- значение - что он отдаст тому, кто его нашёл.
Каждый символ, попадая в модель, кладёт свои ключ и значение в общий список и больше их не меняет. А дальше текущий символ берёт свой запрос и сравнивает его с ключами всех, кто был до него. Чем ближе запрос к ключу, тем больше вес. Веса нормируются, чтобы дать в сумме единицу, и с ними складываются значения найденных символов.
Близость запроса к ключу меряется скалярным произведением: перемножили покоординатно, всё сложили. Одно число на пару. Чем оно больше, тем сильнее совпали направления - при условии, что длины векторов сопоставимы, само по себе скалярное произведение растёт и от длины.
Вот это сравнение в коде. КвО - запрос текущего символа, КэшК - список ключей всех предыдущих, Модель.МасштабВнимания - постоянный множитель, равный единице, делённой на корень из размера головы; он не даёт числам разрастись вместе с длиной вектора и на смысл не влияет:
Оценки = Новый Массив(Позиция + 1); Для Прошлая = 0 По Позиция Цикл КПрошлой = КэшК[Прошлая]; Скаляр = 0; Для Ном = Начало По Начало + Модель.РГоловы - 1 Цикл Скаляр = Скаляр + КвО[Ном] * КПрошлой[Ном]; КонецЦикла; Оценки[Прошлая] = Скаляр * Модель.МасштабВнимания; КонецЦикла;
Два вложенных цикла и умножение. Всё.
Сама операция знакомая: взвешенная сумма с посчитанными коэффициентами есть в любом отчёте. Необычно тут одно - коэффициенты никто не задавал. Матрицы, из которых получаются запрос, ключ и значение, подобрались при обучении сами, и какой символ на какой посмотрит, заранее не знает и автор.
Контекст: сколько символов назад модель вообще способна видеть
Контекст - это максимальная длина последовательности, которую модель физически может держать. У меня 96 символов. Не 96 "примерно", а ровно: таблица векторов позиций имеет 96 строк, и для 97-й позиции строки просто нет.
Это отвечает на вопрос, почему у больших моделей контекст указан в характеристиках наравне с числом параметров и почему за длинный контекст берут деньги. Ниже, в разделе про KV-кэш, будет видно, во что он обходится.
Вероятности: как произвольные числа превращаются в доли
На выходе модель выдаёт по одному числу на каждый символ алфавита. У меня 36 чисел. Они произвольные: могут быть 7,3 и -2,1, в сумме что угодно. Их называют логитами.
Чтобы получить из них вероятности, применяют softmax. Под громким названием три действия: каждое число возводится в экспоненту, все результаты складываются, каждый делится на сумму. На выходе 36 положительных долей, дающих в сумме единицу.
Функция ВВероятности(Числа, Температура = 1) Предел = -1000000000; Для Каждого Значение Из Числа Цикл Предел = Макс(Предел, Значение); КонецЦикла; Результат = Новый Массив(Числа.Количество()); Сумма = 0; Для Ном = 0 По Числа.ВГраница() Цикл Э = Exp((Числа[Ном] - Предел) / Температура); Результат[Ном] = Э; Сумма = Сумма + Э; КонецЦикла; Для Ном = 0 По Результат.ВГраница() Цикл Результат[Ном] = Результат[Ном] / Сумма; КонецЦикла; Возврат Результат; КонецФункции
Вычитание максимума перед экспонентой - защита от переполнения, на результат оно не влияет.
Ключевое, что тут надо увидеть: модель не выбирает букву. Она считает распределение по всем 36 символам сразу, а выбор буквы происходит отдельным шагом, снаружи модели. И вот тут появляется температура.
Температура: тем, кто её крутит вслепую
Здесь расходятся представление и механика, поэтому разберу подробно.
Температура не делает модель креативной, потому что она вообще не про модель. Модель посчитала свои 36 вероятностей и на этом закончила работу. Дальше кто-то должен выбрать одну букву из тридцати шести, и температура управляет только этим выбором.
Вот весь выбор, обе ветки:
Если Температура <= 0 Тогда Лучший = 0; Для Ном = 1 По Логиты.ВГраница() Цикл Если Логиты[Ном] > Логиты[Лучший] Тогда Лучший = Ном; КонецЕсли; КонецЦикла; Возврат Лучший; КонецЕсли; Вероятности = ВВероятности(Логиты, Температура); ГСЧ = Новый ГенераторСлучайныхЧисел; Порог = ГСЧ.СлучайноеЧисло(0, 1000000) / 1000000; Накоплено = 0; Для Ном = 0 По Вероятности.ВГраница() Цикл Накоплено = Накоплено + Вероятности[Ном]; Если Накоплено >= Порог Тогда Возврат Ном; КонецЕсли; КонецЦикла; Возврат Вероятности.ВГраница(); // страховка от накопленной погрешности
Три режима, все видны глазами.
Температура 0. Берётся самый вероятный символ, обычный поиск максимума в массиве. Никакой случайности нет вообще. Один и тот же вопрос будет давать один и тот же ответ буква в букву, сколько ни жми.
Температура 1. Бросается кубик по тем вероятностям, что модель насчитала. Распределение берётся как есть, без искажений.
Температура выше 1. Распределение принудительно сглаживается. Посмотрите на строку Exp((Числа[Ном] - Предел) / Температура): перед возведением в экспоненту все числа делятся на температуру. Деление на большее число сжимает разрывы между ними, и после нормировки разница между вероятным и маловероятным уменьшается. У редких символов шансы резко растут.
Теперь смотрите, как это выглядит на живых числах. Два кадра, вопрос один и тот же, отличается только температура. Слева от процентов - символ, справа - полоска.

Распределение при температуре 0: буква "р" набирает 99,9 процента, остальные по нулям
При нуле картина такая: р - 99,9 %, п - 0,1 %, остальные тридцать четыре символа по нулям. Модель зазубрила сорок фраз и не сомневается. Точных нулей там, кстати, нет ни одного: экспонента всегда положительна, просто до десятых долей процента эти символы не дотягивают.

Тот же вопрос при температуре 3: распределение сгладилось, "р" 87,8 процента, "п" 8,3, "д" 1,4
Ставим температуру 3 и жмём ту же кнопку. Логиты модель посчитала ровно те же, изменилось только деление перед экспонентой. И вот результат: р осел до 87,8 %, п вырос до 8,3 %, д получил 1,4 %, а хвост из редких символов поднялся до десятых долей. Один шанс из восьми, что вместо правильной буквы выпадет какая-то другая. На каждом символе ответа.
Отсюда видно, чем всё кончится, если так генерировать целую фразу. Проверяем на пятёрке:

Ответ при температуре 5: "трц: храня зциля вогдменища отнног", 34 символа за 9,7 секунды
зачем нужна транзакция, температура 5 -> трц: храня зциля вогдменища отнног
Промахи копятся: одна неудачная буква уходит обратно на вход, следующая считается уже по испорченному началу, и через десяток символов текст рассыпается. При нуле та же модель на тот же вопрос отвечает нормальной фразой.
Практический вывод. Нужна воспроизводимость - температура нулевая. Иначе один и тот же запрос будет давать разные ответы без всякой причины, и вы будете искать ошибку в промпте там, где её нет. И никакой "креативности" в этой ручке нет: есть степень готовности взять не самый вероятный вариант. Всё, что вы получаете, повышая температуру, - рост доли редких продолжений. Иногда они выглядят как оригинальная мысль. Чаще как "вогдменища".
Инференс и обучение: что происходит внутри 1С, а что снаружи
Два слова, которые часто путают.
Обучение - это подбор весов. Модели показывают текст, она предсказывает следующий символ, ошибку измеряют, и все 21 920 чисел чуть-чуть двигают в сторону меньшей ошибки. Потом ещё раз. И ещё двенадцать тысяч раз.
Инференс (по-русски вывод) - это использование готовых весов. Числа больше не меняются, идёт только счёт.
Внутри 1С у меня только инференс. Обучение сделано снаружи, на PyTorch: 40 пар вопрос-ответ про объекты 1С, корпус на 2 867 символов, двенадцать тысяч шагов, около восьми минут на обычном процессоре. Готовые веса сложены в бинарный файл и лежат макетом внутри обработки, поэтому файл самодостаточный и класть рядом ничего не надо.
Писать обратное распространение ошибки на BSL я сознательно не стал. Это примерно четыреста строк хрупкого кода, и один прогон обучения занимал бы часы. Для объяснения механики он ничего не добавляет: интересное происходит на выводе.
Отсюда честная оговорка про корпус. Сорок шаблонных пар на 21 920 параметрах - это модель запоминает фразы. Обобщения там нет и взяться ему неоткуда. Модель выучила фразы и подставляет их по началу вопроса. Если сказать иначе, зритель переоценит увиденное.
Галлюцинация: почему она неизбежна по устройству
Вернёмся к котлетам.

Ответ на вопрос вне обучения: "документ права документ?", 24 символа за 7,2 секунды, температура 0
Температура ноль, то есть случайности нет вообще: на каждом шаге брался самый вероятный символ. Модель не бросала кубик и не фантазировала, она честно выбирала лучшее из того, что насчитала.
Посмотрите ещё раз на цикл генерации:
Пока СтрДлина(Ответ) < МаксСимволов И Сост.Позиция < Модель.Контекст Цикл Токен = ВыбратьТокен(Итог.Логиты, Температура); Симв = Модель.АлфавитСимволов[Токен]; Если Симв = Символы.ПС Тогда Прервать; КонецЕсли; Ответ = Ответ + Симв; Итог = ШагМодели(Сост, Токен); КонецЦикла;
Найдите здесь место, куда можно вставить "не знаю".
Его нет. ШагМодели обязана вернуть 36 чисел, ВВероятности обязана превратить их в доли, дающие в сумме единицу, ВыбратьТокен обязан вернуть какой-то номер. Ни одна из этих функций не имеет права отказаться. Пустого ответа в этой конструкции физически не существует: даже на полном шуме на входе распределение получится каким-то, и максимум в нём найдётся.
Вот и весь механизм галлюцинации. Модель не врёт и не выдумывает, у неё нет для этого ни намерения, ни возможности. Она продолжает последовательность, потому что ничего другого делать не умеет. На знакомом вопросе продолжение совпадает с правдой, на незнакомом расходится, а внешне эти два случая неотличимы.
Тут напрашивается возражение: большие модели ведь умеют говорить "не знаю". Умеют, и это ничего не меняет. Фразу "не знаю" в них вложили обучением, как правильное продолжение для определённого типа вопросов. Она такой же сгенерированный текст, как любой другой, и появляется она не потому, что модель заглянула в себя и не нашла знания. Сам механизм остался прежним: на каждом шаге считается распределение и берётся продолжение. Обучение меняет, какое продолжение победит, но не отменяет того, что продолжение будет всегда.
Честная оговорка, чтобы вы не поверили мне на слово больше, чем следует. Я утверждаю, что уверенность не связана со знанием, опираясь на текст ответа про котлеты. Распределение вероятностей на этом прогоне я не снимал. Может оказаться, что там верхний символ набрал не 99 %, а 40 %, и тогда формально сомнение в числах было, просто наружу оно не вышло. Кнопка "Какая буква следующая" распределение показывает, так что проверить это может любой, кто скачает саму обработку. Мне самому интересно, что там.
Отсюда практический вывод для всех, кто прикручивает LLM к учётной системе. Уверенный тон ответа не несёт информации о его правильности: тон задаётся формой обучающих текстов, а правильность - совпадением вопроса с тем, что в весах. Проверять надо каждый ответ, у которого есть последствия.
KV-кэш: почему длинный контекст стоит денег
Главная функция называется ШагМодели, и она обрабатывает ровно один символ за вызов. Всё, что модель помнит о предыдущих символах, лежит в двух массивах: Сост.КэшК и Сост.КэшV. Это и есть KV-кэш.
Устроен он так. На каждом шаге для текущего символа считаются два вектора, ключ и значение, и дописываются в конец кэша. Дальше внимание считается по всему кэшу целиком.
Сост.КэшК[НомСлоя].Добавить(КаО); Сост.КэшV[НомСлоя].Добавить(ВэО);
Две строки, а без них вся затея не работает. Без кэша модели пришлось бы на каждый новый символ заново прогонять весь предыдущий текст. В разобранном ниже прогоне 79 шагов, средняя позиция около сорока, значит работы стало бы примерно в сорок раз больше: вместо двенадцати секунд около восьми минут. Это прикидка на бумаге, кэш я не отключал и не мерил.
Теперь понятно, почему у любого облачного сервиса длинный контекст стоит дороже. Кэш растёт с каждым символом, и внимание на каждом следующем шаге перебирает всё, что накопилось. Посчитаем на моей модели: на первом символе внимание считает одно скалярное произведение на голову, на девяносто шестом - девяносто шесть. В умножениях это 128 против 12 288 за шаг, при неизменных 17 536 на матрицах весов. То есть к концу контекста один символ стоит почти вдвое дороже, чем в начале, и это при контексте в жалкие 96 символов. У больших моделей он в тысячи раз длиннее.
Заодно снимается ещё один вопрос, который обычно требует абзаца объяснений. В книжках про трансформеры пишут про "причинную маску": модель не должна подглядывать вперёд. При пошаговой обработке маска не нужна вовсе. Будущих позиций в кэше просто ещё нет.
Восемь принципов, по которым это собрано
Порядок шагов внутри ШагМодели, коротко. Три пункта из восьми выше не разбирались, они с пояснениями.
- Текст превращается в числа через
Соответствие. - К вектору символа прибавляется вектор его позиции, одной строкой:
СложитьВекторы(Модель.ЭмбТокенов[Токен], Модель.ЭмбПозиций[Позиция]). Без этого модель не отличит "кот" от "ток". Внимание складывает прошлые векторы с коэффициентами, а сумма от порядка слагаемых не зависит - значит порядок надо занести прямо в вектор, иначе он теряется. - Внимание: запрос текущего символа сравнивается с ключами предыдущих, их значения смешиваются в полученных пропорциях.
- Полносвязный слой с ReLU. Вектор растягивается вдвое, прогоняется через нелинейность, сжимается обратно. Нелинейность выглядит так:
Если Скрытый[Ном] < 0 Тогда Скрытый[Ном] = 0; // ReLU, вот и вся нелинейность КонецЕсли;
Почему без этой строки всё разваливается. Умножение на матрицу и сложение векторов подчиняются обычной алгебре: два умножения подряд можно свернуть в одно, посчитав произведение матриц заранее. Так схлопнется любая цепочка, и вся модель на два слоя станет равна одному умножению на одну матрицу. Обнуление отрицательных сворачиванию не поддаётся, и только поэтому слои остаются разными слоями.
- Нормализация. После каждого блока вектор делится на собственную длину и растягивается обучаемым коэффициентом. Причина простая: слои складывают свой результат с тем, что пришло на вход, и числа от слоя к слою растут. Без приведения к общему масштабу через два слоя они разъезжаются на порядки, и обучение перестаёт сходиться.
- На выходе вероятности всех символов алфавита - 36 чисел; буквы среди них нет.
- Ответ рождается циклом: предсказали символ, подставили обратно на вход, предсказали следующий.
- KV-кэш хранит ключи и значения прошлых символов, чтобы не считать их заново.
Больше внутри ничего нет. И вот теперь можно вернуться к заголовку: в ШагМодели ровно три Если - обнуление отрицательных в ReLU, пропуск ничтожных весов внимания и служебная выгрузка карты внимания для показа. Ни один из них не смотрит, какая это буква.
Проверка арифметикой: сходятся ли цифры на экране
Тут стоит остановиться и проверить, что я вам не рассказываю сказку. Модель сама считает свои умножения, и это число легко перепроверить руками.
За один шаг модель умножает вектор на шесть матриц весов в каждом из двух слоёв и один раз на выходную таблицу:
2 × (4 × 32 × 32 + 2 × 64 × 32) + 36 × 32 = 17 536
17 536 умножений на один шаг. Это же число обработка печатает сама, оно видно на кадре с кнопкой "Что внутри" выше. Теперь берём показания счётчика из всех четырёх прогонов этой статьи и делим:
| Прогон | Умножений | / 17 536 | Промпт | Ответ |
|---|---|---|---|---|
| регистр накопления, полный ответ | 1 385 344 | 79 | 35 | 44 |
| как жарить котлеты | 876 800 | 50 | 25 | 25 |
| транзакция при температуре 5 | 1 122 304 | 64 | 29 | 35 |
| один проход, кнопка "Какая буква следующая" | 631 296 | 36 | 36 | 0 |
Четыре деления, четыре целых числа, ни одного остатка.
Раскладка считается так. Промпт собирается строкой "в: " + вопрос + "?" + перевод строки + "о:", то есть обёртка добавляет к вопросу семь символов. "что такое регистр накопления" это 28 знаков, плюс семь - вот и 35 шагов. Ответ модель выдаёт с ведущим пробелом, который при выводе срезается, поэтому на 43 показанных символа приходится 44 шага. Складываем: 79.
Проверьте вторую строку сами: "как жарить котлеты" это 18 знаков, плюс обёртка 25, ответ 24 символа плюс пробел 25, всего 50. Сходится.
Последняя строка стоит отдельного взгляда. Кнопка "Какая буква следующая" не генерирует ничего, она прогоняет только промпт и останавливается. Шагов там 36 при тех же 35 у соседей: у этой кнопки промпт заканчивается на "о: " с пробелом, обёртка на один символ длиннее. И вот этот единственный проход по вопросу из 28 знаков стоит 631 296 умножений и почти пять секунд.
Из этого видно вещь, которую по интерфейсу чата не заметишь: вопрос стоит примерно столько же арифметики, сколько ответ. Каждый символ вашего промпта - это отдельный полный проход через все матрицы модели.
Тут сразу предупрежу возражение. В прайс-листах у провайдеров входные токены стоят в разы дешевле выходных, и кажется, что я сказал неправду. Умножений там поровну, разница в том, как их можно выполнить. Символы вопроса известны сразу все, поэтому железо считает их одной большой пачкой и загружает видеокарту целиком. Ответ так посчитать нельзя: следующий символ зависит от предыдущего, и шаги идут строго по одному. Одна и та же работа, разная загрузка железа, отсюда и разные цены. У меня в 1С пачки нет вообще, поэтому вопрос и ответ стоят ровно одинаково.
И оговорюсь про сам счётчик: он считает умножения на матрицах весов и не считает скалярные произведения внутри внимания. Реальное число больше, и растёт оно по мере заполнения кэша.
Почему это так медленно и что с этим можно сделать
Сто с небольшим тысяч умножений в секунду - смешная цифра. Видеокарта делает десятки триллионов. Разрыв в восемь порядков, и вот откуда он берётся.
В 1С каждое число - объект. Массив это коллекция ссылок на объекты, сплошного куска памяти с числами подряд под ним нет, плоского буфера float в платформе не существует. Отсюда следует остальное: векторные инструкции процессора (SIMD, одна команда сразу над пачкой чисел подряд) требуют непрерывной памяти, а её тут не бывает. Видеокарты нет. Потоков внутри модуля нет.
Цену этого я замерил явно. 64 000 умножений в двух вариантах:
| Как хранятся веса | Время |
|---|---|
| байтами в буфере, разворот байта в число в каждом умножении | 688 мс |
| развёрнуты в обычные массивы чисел один раз при загрузке | 266 мс |
Разница в 2,6 раза, и она вся ушла на разворот байта в число. Поэтому в загрузчике тензор разворачивается один раз при загрузке, вместо миллиона раз внутри цикла умножения.
Сверьте с прогонами выше, и цифры не сойдутся: 266 мс на 64 000 умножений это 240 тысяч в секунду, а модель показывает 110-122 тысячи. Половина времени уходит на то, чего счётчик не считает: скалярные произведения внимания по всему кэшу, сложение векторов, корень в нормализации, экспоненту в softmax на каждый символ алфавита. Замер в таблице меряет чистый внутренний цикл; полный шаг модели тяжелее.
Второй замер - про цену размера. Я собрал вторую версию обработки, с моделью на 76 608 параметров вместо 21 920. Код там тот же самый, отличается только число параметров: длина вектора 64 вместо 32, а матрицы квадратные, поэтому умножений на символ она делает почти вчетверо больше. Один и тот же ответ длиной 43 символа:
| Модель | Время |
|---|---|
| 21 920 параметров | 8,3 с |
| 76 608 параметров | 28,3 с |
Оба числа сняты одним заходом на одной машине. На скриншотах выше та же малая модель показывает 12-13 секунд, но это другой день и другая загрузка машины, и сравнивать размеры между заходами нельзя.
Версия эта у меня собрана и работает, но в карточку обработки я её класть не стал: два файла рядом заставляют выбирать вслепую, кто-то заберёт медленную и решит, что обработка тормозит. Наберёт статья десять плюсов - выложу вторым файлом, и разницу можно будет прогнать руками, а не по моей таблице.
Вот это и есть весь разговор про видеокарты, сведённый к одному наблюдению: устройство не поменялось, поменялся объём чисел, и он сразу упёрся в железо.
Отсюда следует потолок. Посчитаем по своей же формуле: у меня 17 536 умножений на 21 920 параметров, коэффициент примерно 0,8. Модель на сто миллионов параметров потребует порядка восьмидесяти миллионов умножений на символ. При наших ста с небольшим тысячах в секунду это больше десяти минут на один символ. Реалистичный предел для встроенного языка - 1-10 миллионов параметров, и то с оговорками. Про модели уровня GPT разговор даже не начинается.
Уточню: расчёт построен на двух точках, 21 920 и 76 608, дальше идёт прямая экстраполяция. На порядок-два ей верить можно, на пять порядков вверх её никто не проверял.
При этом кое-что на таком потолке живёт, и различие тут в типе задачи. Классификатор делает один проход на весь текст и выдаёт метку. Генератор делает проход на каждый символ ответа. Разница равна длине ответа в символах, то есть десятки и сотни раз при той же модели. Поэтому в чистом BSL остаются задачи без генерации: классификация обращений и статей затрат, извлечение ИНН и сумм, поиск похожей номенклатуры, нормализация к справочнику. Диалог, суммаризация и знания о мире - нет. Оговорюсь и здесь: ни одного такого классификатора я пока не собрал, это расчёт, а не отчёт о работе.
Если задача у вас настоящая, а не учебная, честный путь другой: готовая модель вроде ru-e5-small через ONNX Runtime во внешней компоненте. В сотни раз быстрее и без своего обучения. Только это уже не чистый BSL, и объяснять там нечего.
История про метод, которого не существует
Один сюжет из разработки, ради тех, кто пишет код по описанию формата.
Веса лежат в бинарном макете, целые числа там по четыре байта, младший первым (это и называют little-endian). В первой версии загрузчика я читал их так:
Значение = Буф.ПолучитьЦелое32(Позиция, ПорядокБайтов.LittleEndian);
Метод выглядит абсолютно правдоподобно. Логично называется, у соседних типов платформы такие есть, в документации по формату он подразумевается сам собой. Проблема одна: у БуферДвоичныхДанных его не существует. Выяснилось это запуском на живой платформе.
Замена заняла три строки:
Функция Цел32(Буф, Поз) Возврат Буф[Поз] + Буф[Поз + 1] * 256 + Буф[Поз + 2] * 65536 + Буф[Поз + 3] * 16777216; КонецФункции
Обращение к байту по индексу есть у буфера всегда, поэтому такая сборка числа работает на любой версии платформы, где буфер вообще существует.
Раз уж зашла речь про версии, отвечу заранее. Обработка требует 8.3.27.1606, и дело не в коде. Сам код заработает и на более старых платформах, ему хватит БуферДвоичныхДанных, а это 8.3.14. Дело в самом файле .epf: он сохранён конфигуратором 8.3.27, и на 8.3.21 такой файл просто не откроется. Нужен запуск на своей версии - пересохраните у себя, весь исходный текст модуля лежит внутри файла и читается в конфигураторе.
Мораль истории простая и к теме статьи прямо относится. Код, написанный по описанию формата, проверяется запуском, чтением он не проверяется. Тем более если часть этого кода подсказала языковая модель: она сгенерирует несуществующий метод с той же уверенностью, с какой моя обработка отвечает про котлеты. Механизм, как вы теперь знаете, у этих двух случаев один.
Три вопроса, которые мне зададут, отвечаю сразу
"Раскидайте умножение по фоновым заданиям, в 1С так и распараллеливают". Между шагами это не работает: следующий символ зависит от предыдущего, порядок жёсткий. Внутри одного шага теоретически можно, матрицы независимы. Только шаг стоит около сотой доли секунды, а передача массивов в фоновое задание и обратно идёт через сериализацию и обходится дороже самого счёта. Померить я это не пробовал, так что если у кого-то есть цифры, приносите.
"Посчитайте умножение матриц запросом к СУБД, сервер такое делает быстро". Идея хорошая, и для одного большого умножения она может выиграть: СУММА(А.Значение * Б.Значение) с группировкой сервер считает на своей стороне. Проблема в том, что шагов восемьдесят пять, и на каждом надо положить вектор во временную таблицу и забрать результат. Обмен с сервером на каждый шаг съест выигрыш, а на файловой базе выигрывать вообще нечего. Проверять я это не стал, и это, пожалуй, самый интересный неотработанный ход.
"Обучающий скрипт приложен?" Нет, в комплекте только сама обработка. Обучение делается снаружи, на PyTorch, и всё, что нужно для повторения, названо выше: корпус из сорока пар, размеры модели, число шагов, формат весов.
Что забрать с собой
Внутри языковой модели нет понимания текста. Есть таблица чисел, подобранных перебором, и порядок, в котором на них умножают. Правил про язык там никто не писал.
Уверенность модели не связана со знанием. Она обязана продолжить последовательность и продолжает её всегда, есть ли в весах что-то по теме вопроса или нет.
Температура управляет выбором буквы, до модели она вообще не дотягивается. Ноль даёт воспроизводимость, единица - честный кубик, выше единицы - искусственно раздутые шансы редких вариантов.
Промпт стоит той же арифметики, что и ответ. Каждый его символ - полный проход через все матрицы.
Обработка лежит отдельной карточкой: Локальная LLM на встроенном языке 1С. Веса внутри файла макетом, класть рядом ничего не надо, наружу она не ходит вообще. Открывайте в Конфигураторе и ставьте точку останова в ШагМодели: в модуле объекта 471 строка вместе с комментариями, и они читаются.
И вопрос, на который у меня нет ответа. Мой потолок для чистого BSL - миллион-десять миллионов параметров, дальше упирается физика языка: каждое число объект, сплошных буферов чисел нет, векторных инструкций процессора нет. Кто считает, что потолок выше, расскажите, за счёт чего.
Один ход я проверять не стал и честно это отмечаю. Замер 688 против 266 миллисекунд показал только одно: разворачивать байт в число внутри цикла дорого. Он ничего не говорит про целочисленную арифметику как таковую, и вполне может оказаться, что счёт целыми в буфере с редким применением масштаба быстрее того, что я собрал. Если у кого-то дойдут руки померить - интересно, что получится.
Другие наши инструменты для работы с нейросетями:
- Выгрузка метаданных для LLM - структура вашей конфигурации в виде, который модель читает без пересказа: Markdown и JSON с готовыми таблицами запроса.
- Матрица прав доступа для нейросети - кто и что реально видит в базе. Первое, что спрашивают, когда доступ получает внешний инструмент.
- Анализ кода внешних обработок - разбор чужого кода лексером и парсером на чистом BSL. Тот же принцип, что и здесь: всё считается внутри 1С, наружу не уходит ничего.
Вступайте в нашу телеграмм-группу Инфостарт