Сгенерировано с помощью gigaChat
Сгенерировано с помощью gigaChat

Один из моих коллег сказал когда-то, что "база данных - это хранилище, а не считалище!". Эту фразу я вспоминал регулярно, пока проводил свое маленькое исследование. Целью данной статьи является описание практического опыта эффективного решения одной из задач ML на существующих аппаратных ресурсах, без аренды/покупки дорогостоящих GPU.

Лирическое вступление

Скупость, лень и инакомыслие сподвигли меня к написанию этой статьи. Смеркалось, жаркий день бледнел неуловимо... Я неосторожно задумался о способах оптимизации векторного представления лексических токенов (WTE) в языковых моделях, когда ощутил легкое раздражение. Это была жадность....при мысли о необходимости подбора новой мощной видеокарты, которая возможно не подойдет к моему десктопу и придется менять все-все... - к жадности подключилась лень. Чтобы как-то переключиться от неприятных мыслей, я стал размышлять от обратного. Ведь мы хотим хранить и обрабатывать большой объем данных. Но с этой задачей человечество относительно успешно справляется и без GPU. У нас есть BigData, есть Spark RDD (Resilent Distributed Dataset). Да, в скорости обработки этот механизм будет явно уступать парку GPU, но разница в том, что эти ресурсы уже есть - у организаций свои платформы данных, у нас с вами - возможность поставить и использовать СУБД на обычный ноутбук. И тут я вспомнил про статью "С новым годом: GPT в 500 строках на SQL" и понял что это может быть решением.

Постановка задачи

Реализовать токенизацию для GPT исключительно на ресурсах Clickhouse

Комментарии к задаче

  1. А почему именно токенизацию, а не WTE к примеру? Решил начать с относительно более простой задачи, а далее двигаться по всему треку обучения модели вплоть до инференса. Ну т. е. рассчитываю, что это будет цикл статей.

  2. А почему только на ресурсах Clickhouse, что мешает использовать простенький python‑вский скриптик который все посчитает, а потом положит в БД? Потому как теряется смысл задумки. Скрипт будет выгружать содержимое таблиц/таблицы в память и далее обсчитывать. Если содержимое таблиц превышает размер оперативной памяти, то уже ОС начинает подключать swap, как следствие — деградация производительности. Сюда же добавим транзакционные издержки на извлечение данных.

  3. Почему именно Clickhouse.

    3.1) Объективно быстрая распределенная дисковая реляционная СУБД в сравнении с другими аналогами. Ну, и были мысли в дальнейшем исследовать механизм репликации Клика для организации распределенных вычислений.

    3.2) Более богатый синтаксис SQL Clickhouse.

Коротко про токенизацию.

В словаре Эллочки-людоедки из романа И. Ильфа и Е.Петрова было 30 слов (задумался, все-таки слов, или выражений?). У моделей примерно также, только слов чуть побольше (50-100 тыс.) И это не слова в привычном понимании, а скорей минимальная единица текста которая часто встречается. Наиболее простое и более подробное объяснение встретил вот здесь

Шаг 1. Загрузка датасета для обучения.

Используем kaggle. Взял достаточно несложный датасет с новостями (около 209 тыс. коротких новостей). Ближе к финалу статьи, понял что могу обрабатывать существенно большие объемы данных. Но для экспериментов - в самый раз.

Создал тестовую БД, с именем GPT. Вот DDL-ки таблиц, которые мы будем использовать:

create table GPT.ds_news_category
(
	link String,
	headline String,
	category String,
	short_description String,
	authors String,
	date Date
)
ENGINE = MergeTree
PRIMARY KEY link;

create table GPT.vocabulary
(
	token String
)
ENGINE = MergeTree
PRIMARY KEY token;

create table GPT.pre-tokens
(
	word String,
	count UInt32
)
ENGINE = MergeTree
PRIMARY KEY word;

Данные в БД загрузил через clickhouse-console вот так:

clickhouse-client --user=default --password=haha --query="INSERT INTO GPT.ds_news_category FORMAT JSONEachRow" < News_Category_Dataset_v3.json

Шаг 2. Предварительная подготовка токенов (нормализация).

Весь текст переведен в нижний регистр, убраны все знаки препинания. Для токенизации использовал три поля таблички датасета: headline, category, short_description

truncate TABLE GPT.pre_tokens;

insert into GPT.pre_tokens (word,count)
with words as (
with ds as (
	select 
	splitByWhitespace(lower(headline)) AS source
	from GPT.ds_news_category
	
	union all
	
	select 
	splitByWhitespace(lower(category)) AS source
	from GPT.ds_news_category
	
	union all
	
	select 
	splitByWhitespace(lower(short_description)) AS source
	from GPT.ds_news_category

)
select replaceRegexpAll(arrayJoin(source),'[[:^alpha:]]','') as word
from ds
where length(word)>0
)
select distinct(word), count()
from words
group by word
order by count() desc;

В итоге получилось примерно вот такое содержимое (всего порядка 112 тыс уникальных слов):

Обратите внимание, что мы сразу считаем частоту (поле count)

Шаг 3. Генерация словаря токенов.

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

-- первый запуск
insert into GPT.vocabulary (token)
with letters as (
	select 
	splitByRegexp('',word) w
	from GPT.pre_tokens pt 
)
select distinct(arrayJoin(w))
from letters;

-- второй запуск
insert into GPT.vocabulary (token)
with max_result as (
with letters as (
	select 
	ngrams(word,2) w
	from GPT.pre_tokens pt 
)
select arrayJoin(w) token, count() c
from letters
where 
token NOT IN (SELECT * FROM GPT.vocabulary)
group by token
having c>1
order by c desc
limit 300
)
select token
from max_result

Вот что получилось в итоге:

Итоги работы и планы

  1. Производительность. На данном этапе замеров не производилось по банальной причине - Clickhouse все задачи выполнял субъективно за промежуток времени в районе 1-2 секунд. Как и упоминал в начале ресурсы моего десктопа достаточно скромны (Core i3, 2.4Ггц, 16 ГБ оперативной памяти). Здесь в планах существенно увеличить датасет.

  2. Очень помогает синтаксис Clickhouse SQL. В классическом ANSI SQL код выглядел бы менее изящнее

  3. Общий план амбициозен конечно - сделать полноценный GPT используя только существующие ресурсы и только на платформе Clickhouse. В том числе и в части взаимодействия с пользователем во время инференса модели, благо у Клика есть свой веб-интерфейс.

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


  1. Akina
    24.09.2025 05:53

    <offtop>

    Один из моих коллег сказал когда-то, что "база данных - это хранилище, а не считалище!".

    Очень жаль. Нет, когда кто-то имеет эффективный инструмент, но не использует его по привычке/незнанию/прочему, это ещё можно как-то принять, но вот гордиться этим - такое даже понять невозможно.

    описание практического опыта эффективного решения одной из задач ML на существующих аппаратных ресурсах

    Ну тут вы (если эта ваша фраза связана с предыдущей процитированной) всё-таки немножко лукавите. Если считать на стороне СУБД, используются только ресурсы сервера БД. Когда вы считаете это на стороне клиента, то добавляются ещё и ресурсы клиентского компьютера. Так что "на существующих аппаратных ресурсах" - выражение не во всех смыслах верное.


    1. raptor
      24.09.2025 05:53

      Очень жаль. Нет, когда кто-то имеет эффективный инструмент, но не использует его по привычке/незнанию/прочему, это ещё можно как-то принять, но вот гордиться этим - такое даже понять невозможно.

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