<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>zatsepin.dev / essays (RU)</title><link>https://zatsepin.dev/essays/</link><description>Эссе и статьи на русском</description><language>ru</language><lastBuildDate>Mon, 17 Aug 2026 17:44:06 GMT</lastBuildDate><item><title>Пишу свой собсвенный Skitch в 2026 #1</title><link>https://zatsepin.dev/essays/4ujepfen8t06183?lang=ru</link><guid isPermaLink="true">https://zatsepin.dev/essays/4ujepfen8t06183?lang=ru</guid><pubDate>Mon, 17 Aug 2026 11:56:28 GMT</pubDate><description><![CDATA[Зацепин Юрий воссоздаёт магический поиск по картинкам из Evernote локально с помощью open-source AI-моделей (семантические эмбеддинги и цветовая кластеризация) без облачных сервисов, доказывая, что современные технологии делают это дёшево и просто.]]></description><content:encoded><![CDATA[<article no-shadow="" class="prose max-w-none prose-stone mx-auto drop-cap"><div class="markdown-styles" data-hash="1pd92os"><style>:host{display:block;position:relative;contain:content;}:host([hidden]){display:none;}</style><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css" media="(prefers-color-scheme:dark)"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0/dist/katex.min.css"></div><div class="markdown-body" data-hash="zvmitd"><p>Пятнадцать лет назад я стал пользователем Evernote. Счастливое время, когда мысли были только о стартапах, Кремниевой долине и технологиях. И хоть сейчас евангелисты Obsidian абсолютно точно уверены, что они впервые создают второй мозг, позволю себе сказать: я создавал second brain в 2010-х с помощью Evernote и Moleskine. А OCR поиск Evernote по картинкам и документам был абсолютно магическим. Не зря, видимо, до этого инженеры работали над решениями OCR для правительства США. </p>
<p>Эта статья о том, как можно собрать локальный поиск по картинкам и фото, который будет работать без больших корпоративных сервисов, без LLM/SaaS и без передачи персональных данных наружу.</p>
<p><strong>Чем был для меня Evernote</strong></p>
<p>Конечно, как и сейчас, за довольно простым, но хорошим продуктом должно стоять что-то большее. Идея или дистрибьюшен. Ведь это же только лишь сервис для написания заметок и ничего больше. Для меня это была идея о том, что за океаном русский бородатый мужик Степан Пачиков, технарь, взял, сделал и заработал на этом какие-то сумасшедшие деньги. Я им пользовался, советовал друзьям, участвовал в его хакатонах и даже там получал призовые. Помню отсыпали довольно много кредитов на AWS.</p>
<p>Из всего набора фичей Evernote самыми ценными были всего три на мой взгляд. Но именно они удерживали меня как пользователя вплоть до 2022 года — года, когда я отказался от SaaS сервисов крупных компаний. Больше десяти лет подписки...</p>
<p>Итак, что это были за якорные фичи:</p>
<ol>
<li><p><strong>Поиск по документам, картинкам и фото</strong></p>
<p> Чертовски полезная вещь. Понятно, что при правильной организации меток и папок можно находить любую, даже самую старую заметку или документ, просто двигаясь по какой-то системе. Но если к системе добавить ещё и очень точный поиск, то процесс будет куда приятнее. У меня было много медицинских документов, которые было очень просто находить по ключевым словам, присутствующим на картинке или скане. И мне не надо было руками проводить предварительную индексацию и организацию ради самой организации. Просто добавление одной метки — и OCR-поиск делал своё дело. Да, поиск на тот момент был именно что OCR. Но к этому мы ещё вернёмся.</p>
</li>
<li><p><strong>Будильник на заметки</strong></p>
<p> У меня было несколько удачных применений этой фичи: будильники срабатывали пару раз в год и помогали мне не пропустить нужный момент, возвращая контекст того, что мне надо было сделать. Сейчас я использую для этого почту.</p>
</li>
<li><p><strong>Skitch (аннотатор скриншотов)</strong></p>
<p> В рамках моей профессии нужно коммуницировать с другими людьми, показывая тот или иной элемент продукта и оставляя к нему комментарии. Аналогичные сервисы были и есть. Аналог для Linux до сих пор не умеет делать нормальные стрелочки.</p>
</li>
</ol>
<p>Недавно пробегая по скриншотам в поиске одного нужного на своем пк, подумал, как было бы прикольно если был бы поиск из Evernote. И да я знаю, что подобных решений было и есть множество. Причём речь может идти не только о коммерческих продуктах, но и open source, просто поищите на GitHub. Но все это как это всегда и бывает "не то" "было лучше". Хотя конечно лучше то и не было, поиск на Evernote работал по принципу OCR, очевидно раз компания все еще существует, то смею предположить, что он был улучшен до гибридного поиска. То есть совмещения OCR (поиска по тексту распознаному из картинки)  и семантического поиска, то есть поиска по embedding'ам, которые были получены из картинки с помощью какой-то модели.
Речь пойдет о небольших моделях, которые можно запустить на обычном пользовательском железе и они имеют очень ограниченный фокус на то что они делают.</p>
<h4 id="что-я-хочу-от-поиска-по-картинкам">Что я хочу от поиска по картинкам?</h4>
<p>Если на картинке изображена кошка или металический люк, то я бы мог так и написать свой запрос и получить подобные картинки. Очень хотелось бы искать картинки по цветовой гамме. Скажете зачем, но это невероятно удобно, в моих кейсах когда я точно помню дизайн какого-то сервиса и поиск по запросу это profile form, может и не помочь, а вот фиолетовый цвет + profile form, результат будет найден. Хочется искать используя не только английский текст, но и русский в запросах. Чтобы я мог написать metal grate или металлический люк и получил бы схожий результат. Конечно чтобы поиск учитывал текст на картинке, если текста там много, но и смысл этого текста. К примеру я сделал скриншот с YouTube рецепта теста для пиццы, он был написан маркером на доске. Текст на картинке содержит слова вода, мука и соль. Желательно бы его искать по запросу "рецепт пиццы" или просто "рецепт". Также поиск должен быть быстрым. Ок, я готов ждать долгой индексации и предварительного анализа базы картинок, но сам поиск должен быть около мгновенным. Итак, возможно ли это сделать с использованием современных моделей, не используя внешних сервисов и API.</p>
<h4 id="шаг-1-семантический-поиск">Шаг 1. Семантический поиск.</h4>
<p>Или поиск по embedding'ам картинок и embedding'у полученному из текстового запроса пользователя. 
Для решения нам нужна мультимодальная модель эмбеддингов и желательно с лицензией MIT/Apache. И такая есть это Nomic Embed Vision, как и любая трендовая модель на основе трансформером и BERT-архитектуры. Прелесть модели для нашей инженерной задачи в том, что текст и картинки на входе, дают вектора из одного пространства. И мы можем искать текст по картинке, картинку по картинке или картинку по тексту. Из минусов, русский язык работает не самым лучшим образом в запросах, это конечно решим тоже, но позже.</p>
<p>Давайте разберемся как я буду проверять качество принятых моделей в пайплайне поиска. А буду качественным методом нравится или не нравится на моей локальной папке со скриншотами. Их всего порядка 500. Глобальная задача скормить всю мою базу знаний, заметок, скриншотов, рефов, но пока ограничимся примером поиска по скриншотам. У меня был список картинок, которые я хотел найти из своей папки скриншотов. И поиск должен их найти. Никаких сложных бенчей и методологии, я просто должен кайфануть от поиска.
Итак берем rust, берем onnx модели nomic vision и смотрим что получается.</p>
<p><img src="https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/lubo_1785192719485729924_08y66ee37h.png" alt=""></p>
<p>Первый же результат отработал идеально, это конечно было сопадением, но удачным. У меня был скриншот с YouTube ролика, как можно сварить люк на паркинге, чтобы в нем УК могла бы похоронить насос. И поиск этой картинки был мгновенным, имея на руках лишь Semantic search с использованием одной единственной модели.
Сами вектора embeddings можно хранить в любой бд с поддержкой векторов и поиска по ним. У SQLite есть плагин, есть еще LanceDB. Они подходят идеально потому что не имеют избыточной сложности в поддержке и работе с ними. Помним что SQLite установлен вообще на всех смартфонах в мире и абсолютно булетпруфная база данных.</p>
<p>Проблема была только с запросами на русском языке, решать проблему я взялся в лоб. А именно переводом запросов с русского на английский язык и подобрал маленькую модель для пары языков c русского на английский. 
<a href="https://huggingface.co/Teradata/opus-mt_tiny_rus-eng">https://huggingface.co/Teradata/opus-mt_tiny_rus-eng</a>
Запросы стали работать значительно лучше. Конечно мы на каждом таком шаге теряем в качестве, если бы модель уже была мультиязычная, было бы лучше, но идем простым путем. Для обучения аналогичной модели нужен не только стек из 16+ H100/H200, но и длительная работа с датасетами, что неприемлемо для моей задачи. Но небольшими силами (увеличением цепочки из моделей) целевой результат вырос и вообще стало возможным искать металлический объект, словом "метал", а не только "metal".</p>
<h4 id="шаг-2-поиск-по-цветам">Шаг 2. Поиск по цветам.</h4>
<p>Это моя давняя мечта о том, как я бы хотел искать в своей библиотеке референсов с изображениями. И цвета абсолютно идеально укладываются в векторном виде. И чтобы сделать такой поиск надо сделать две вещи. Первое это вытащить доминантные цвета или своего рода палитру из картинки. Второе уложить палитру цветов в виде вектора. И дальше можно просто искать близкие вектора, абсолютно как я сделал до этого с embeddings от nomic vision модели. Чтобы вытащить палитру можно использовать разные алгоритмы, но как я люблю подходить к таким задачам, будет брать самое простое решение. Главное чтобы оно могло дать значимо хороший результат. Есть k-means clustering, его и возьмем. Существует несколько хороших библиотек для Rust, я взял <a href="https://crates.io/crates/kmeans_colors">https://crates.io/crates/kmeans_colors</a>
Для каждого изображения k-means извлекает 5 доминирующих цветов в цветовом пространстве Lab, которые склеиваются в один плоский вектор и индексируются в LanceDB. Когда пользователь ищет одновременно по тексту и цвету, система сначала выполняет семантический поиск (векторный или гибридный), а затем переранжирует результаты с учетом поиска по цвету. В этой оценке семантическая релевантность первична, а цветовая близость — уточняющий фактор.</p>
<p><img src="https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/screenshot_from_2026_07_25_21_09_38_glrt2nl8i6.png" alt="">
Бам! Это работает конечно же в совокупности с семантическим поиском. К примеру я хочу поискать login UX скриншоты. И помню что нужный мне сайт оранжевый или красный.</p>
<p><img src="https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/screenshot_from_2026_07_25_21_12_07_nlpwqda8rc.png" alt="">
Пушка булочка. Ровно то, что я и хотел. В моем следующем youtube видео я расскажу, что еще можно добавить в этот пайплайн, чтобы сделать еще лучше. Напомню, что поиск уже работает абсолютно волшебно и при этом мы даже не сделали OCR, мы еще не распознали текст на картинках и не добавили его в так называемый гибридный поиск.</p>
<p>С момента того, как над Evernote начали работать прошло уже больше 20 лет. И если в начале пути этого SaaS технология поиска по картинкам была действительно заградительным оружием против конкурентов, то со временем технология стала очень дешевой в разработке и внедрении. Да, конечно, Tesseract OCR стал open source уже тоже десятилетия назад. Но это абсолютная земля и небо, что мы можем запускать сейчас  и тогда локально на машине пользователя и с каким уровнем качества.</p>
<p>Мы вроде бы должны войти в эпоху тысяч и тысяч новых приложений и сервисов, которые могут решать старые проблемы значительно проще, быстрее и лучше. LLM вроде как решили проблему софта и оставили лишь проблему идеи. Но где же эти тысячи новых стартапов? </p>
<p>P.S.</p>
<p>Результат работы можно попробовать скачав приложение по <a href="https://zatsepin.dev/lubo">ссылке</a>. Приложение доступно только для macOS, Linux и Windows. Протестирован пока только на Linux. Я продолжаю над ним работу прямо сейчас и не вижу еще то, что из него получится. Пока появилась рабочая идея, сделать свой Skitch. Только абсолютно с локальными моделями для поиска и для обработки изображений. Если интересно можете подписаться на <a href="https://zatsepin.dev/subscribe">https://zatsepin.dev/subscribe</a> или на YouTube <a href="https://www.youtube.com/@yurizatsepin">https://www.youtube.com/@yurizatsepin</a></p>
</div>
      <script type="text/markdown">
        
Пятнадцать лет назад я стал пользователем Evernote. Счастливое время, когда мысли были только о стартапах, Кремниевой долине и технологиях. И хоть сейчас евангелисты Obsidian абсолютно точно уверены, что они впервые создают второй мозг, позволю себе сказать: я создавал second brain в 2010-х с помощью Evernote и Moleskine. А OCR поиск Evernote по картинкам и документам был абсолютно магическим. Не зря, видимо, до этого инженеры работали над решениями OCR для правительства США. 

Эта статья о том, как можно собрать локальный поиск по картинкам и фото, который будет работать без больших корпоративных сервисов, без LLM/SaaS и без передачи персональных данных наружу.

**Чем был для меня Evernote**

Конечно, как и сейчас, за довольно простым, но хорошим продуктом должно стоять что-то большее. Идея или дистрибьюшен. Ведь это же только лишь сервис для написания заметок и ничего больше. Для меня это была идея о том, что за океаном русский бородатый мужик Степан Пачиков, технарь, взял, сделал и заработал на этом какие-то сумасшедшие деньги. Я им пользовался, советовал друзьям, участвовал в его хакатонах и даже там получал призовые. Помню отсыпали довольно много кредитов на AWS.

Из всего набора фичей Evernote самыми ценными были всего три на мой взгляд. Но именно они удерживали меня как пользователя вплоть до 2022 года — года, когда я отказался от SaaS сервисов крупных компаний. Больше десяти лет подписки...

Итак, что это были за якорные фичи:

1. **Поиск по документам, картинкам и фото**
    
    Чертовски полезная вещь. Понятно, что при правильной организации меток и папок можно находить любую, даже самую старую заметку или документ, просто двигаясь по какой-то системе. Но если к системе добавить ещё и очень точный поиск, то процесс будет куда приятнее. У меня было много медицинских документов, которые было очень просто находить по ключевым словам, присутствующим на картинке или скане. И мне не надо было руками проводить предварительную индексацию и организацию ради самой организации. Просто добавление одной метки — и OCR-поиск делал своё дело. Да, поиск на тот момент был именно что OCR. Но к этому мы ещё вернёмся.
    
2. **Будильник на заметки**
    
    У меня было несколько удачных применений этой фичи: будильники срабатывали пару раз в год и помогали мне не пропустить нужный момент, возвращая контекст того, что мне надо было сделать. Сейчас я использую для этого почту.
    
3. **Skitch (аннотатор скриншотов)**
    
    В рамках моей профессии нужно коммуницировать с другими людьми, показывая тот или иной элемент продукта и оставляя к нему комментарии. Аналогичные сервисы были и есть. Аналог для Linux до сих пор не умеет делать нормальные стрелочки.

Недавно пробегая по скриншотам в поиске одного нужного на своем пк, подумал, как было бы прикольно если был бы поиск из Evernote. И да я знаю, что подобных решений было и есть множество. Причём речь может идти не только о коммерческих продуктах, но и open source, просто поищите на GitHub. Но все это как это всегда и бывает "не то" "было лучше". Хотя конечно лучше то и не было, поиск на Evernote работал по принципу OCR, очевидно раз компания все еще существует, то смею предположить, что он был улучшен до гибридного поиска. То есть совмещения OCR (поиска по тексту распознаному из картинки)  и семантического поиска, то есть поиска по embedding'ам, которые были получены из картинки с помощью какой-то модели.
Речь пойдет о небольших моделях, которые можно запустить на обычном пользовательском железе и они имеют очень ограниченный фокус на то что они делают.

#### Что я хочу от поиска по картинкам?

Если на картинке изображена кошка или металический люк, то я бы мог так и написать свой запрос и получить подобные картинки. Очень хотелось бы искать картинки по цветовой гамме. Скажете зачем, но это невероятно удобно, в моих кейсах когда я точно помню дизайн какого-то сервиса и поиск по запросу это profile form, может и не помочь, а вот фиолетовый цвет + profile form, результат будет найден. Хочется искать используя не только английский текст, но и русский в запросах. Чтобы я мог написать metal grate или металлический люк и получил бы схожий результат. Конечно чтобы поиск учитывал текст на картинке, если текста там много, но и смысл этого текста. К примеру я сделал скриншот с YouTube рецепта теста для пиццы, он был написан маркером на доске. Текст на картинке содержит слова вода, мука и соль. Желательно бы его искать по запросу "рецепт пиццы" или просто "рецепт". Также поиск должен быть быстрым. Ок, я готов ждать долгой индексации и предварительного анализа базы картинок, но сам поиск должен быть около мгновенным. Итак, возможно ли это сделать с использованием современных моделей, не используя внешних сервисов и API.

#### Шаг 1. Семантический поиск. 
Или поиск по embedding'ам картинок и embedding'у полученному из текстового запроса пользователя. 
Для решения нам нужна мультимодальная модель эмбеддингов и желательно с лицензией MIT/Apache. И такая есть это Nomic Embed Vision, как и любая трендовая модель на основе трансформером и BERT-архитектуры. Прелесть модели для нашей инженерной задачи в том, что текст и картинки на входе, дают вектора из одного пространства. И мы можем искать текст по картинке, картинку по картинке или картинку по тексту. Из минусов, русский язык работает не самым лучшим образом в запросах, это конечно решим тоже, но позже.

Давайте разберемся как я буду проверять качество принятых моделей в пайплайне поиска. А буду качественным методом нравится или не нравится на моей локальной папке со скриншотами. Их всего порядка 500. Глобальная задача скормить всю мою базу знаний, заметок, скриншотов, рефов, но пока ограничимся примером поиска по скриншотам. У меня был список картинок, которые я хотел найти из своей папки скриншотов. И поиск должен их найти. Никаких сложных бенчей и методологии, я просто должен кайфануть от поиска.
Итак берем rust, берем onnx модели nomic vision и смотрим что получается.

![](https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/lubo_1785192719485729924_08y66ee37h.png)

Первый же результат отработал идеально, это конечно было сопадением, но удачным. У меня был скриншот с YouTube ролика, как можно сварить люк на паркинге, чтобы в нем УК могла бы похоронить насос. И поиск этой картинки был мгновенным, имея на руках лишь Semantic search с использованием одной единственной модели.
Сами вектора embeddings можно хранить в любой бд с поддержкой векторов и поиска по ним. У SQLite есть плагин, есть еще LanceDB. Они подходят идеально потому что не имеют избыточной сложности в поддержке и работе с ними. Помним что SQLite установлен вообще на всех смартфонах в мире и абсолютно булетпруфная база данных.

Проблема была только с запросами на русском языке, решать проблему я взялся в лоб. А именно переводом запросов с русского на английский язык и подобрал маленькую модель для пары языков c русского на английский. 
https://huggingface.co/Teradata/opus-mt_tiny_rus-eng
Запросы стали работать значительно лучше. Конечно мы на каждом таком шаге теряем в качестве, если бы модель уже была мультиязычная, было бы лучше, но идем простым путем. Для обучения аналогичной модели нужен не только стек из 16+ H100/H200, но и длительная работа с датасетами, что неприемлемо для моей задачи. Но небольшими силами (увеличением цепочки из моделей) целевой результат вырос и вообще стало возможным искать металлический объект, словом "метал", а не только "metal".

#### Шаг 2. Поиск по цветам.

Это моя давняя мечта о том, как я бы хотел искать в своей библиотеке референсов с изображениями. И цвета абсолютно идеально укладываются в векторном виде. И чтобы сделать такой поиск надо сделать две вещи. Первое это вытащить доминантные цвета или своего рода палитру из картинки. Второе уложить палитру цветов в виде вектора. И дальше можно просто искать близкие вектора, абсолютно как я сделал до этого с embeddings от nomic vision модели. Чтобы вытащить палитру можно использовать разные алгоритмы, но как я люблю подходить к таким задачам, будет брать самое простое решение. Главное чтобы оно могло дать значимо хороший результат. Есть k-means clustering, его и возьмем. Существует несколько хороших библиотек для Rust, я взял https://crates.io/crates/kmeans_colors
Для каждого изображения k-means извлекает 5 доминирующих цветов в цветовом пространстве Lab, которые склеиваются в один плоский вектор и индексируются в LanceDB. Когда пользователь ищет одновременно по тексту и цвету, система сначала выполняет семантический поиск (векторный или гибридный), а затем переранжирует результаты с учетом поиска по цвету. В этой оценке семантическая релевантность первична, а цветовая близость — уточняющий фактор.

![](https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/screenshot_from_2026_07_25_21_09_38_glrt2nl8i6.png)
Бам! Это работает конечно же в совокупности с семантическим поиском. К примеру я хочу поискать login UX скриншоты. И помню что нужный мне сайт оранжевый или красный.

![](https://zatsepin.dev/api/files/pbc_pages/4ujepfen8t06183/screenshot_from_2026_07_25_21_12_07_nlpwqda8rc.png)
Пушка булочка. Ровно то, что я и хотел. В моем следующем youtube видео я расскажу, что еще можно добавить в этот пайплайн, чтобы сделать еще лучше. Напомню, что поиск уже работает абсолютно волшебно и при этом мы даже не сделали OCR, мы еще не распознали текст на картинках и не добавили его в так называемый гибридный поиск.

С момента того, как над Evernote начали работать прошло уже больше 20 лет. И если в начале пути этого SaaS технология поиска по картинкам была действительно заградительным оружием против конкурентов, то со временем технология стала очень дешевой в разработке и внедрении. Да, конечно, Tesseract OCR стал open source уже тоже десятилетия назад. Но это абсолютная земля и небо, что мы можем запускать сейчас  и тогда локально на машине пользователя и с каким уровнем качества.

Мы вроде бы должны войти в эпоху тысяч и тысяч новых приложений и сервисов, которые могут решать старые проблемы значительно проще, быстрее и лучше. LLM вроде как решили проблему софта и оставили лишь проблему идеи. Но где же эти тысячи новых стартапов? 

P.S.

Результат работы можно попробовать скачав приложение по [ссылке](https://zatsepin.dev/lubo). Приложение доступно только для macOS, Linux и Windows. Протестирован пока только на Linux. Я продолжаю над ним работу прямо сейчас и не вижу еще то, что из него получится. Пока появилась рабочая идея, сделать свой Skitch. Только абсолютно с локальными моделями для поиска и для обработки изображений. Если интересно можете подписаться на https://zatsepin.dev/subscribe или на YouTube https://www.youtube.com/@yurizatsepin








      </script>
    </article>]]></content:encoded></item><item><title>Как запустить GPU Runtime для Google Collab с помощью Terraform</title><link>https://zatsepin.dev/essays/0ipfipxovlxbu5m?lang=ru</link><guid isPermaLink="true">https://zatsepin.dev/essays/0ipfipxovlxbu5m?lang=ru</guid><pubDate>Wed, 25 Feb 2026 15:32:32 GMT</pubDate><description><![CDATA[Руководство по подключению внешнего GPU к Google Colab. С помощью OpenTofu автоматизация запуска Jupyter Notebook на удалённом сервере с пробросом порта и настройкой доступа.

]]></description><content:encoded><![CDATA[<article class="prose max-w-none prose-stone mx-auto drop-cap"><div class=markdown-styles data-hash=1pd92os><style>:host {
        display: block;
        position: relative;
        contain: content;
      }
      :host([hidden]) {
        display: none;
      }</style><link href=https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css media=(prefers-color-scheme:dark) rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/katex@0/dist/katex.min.css rel=stylesheet></div><div class=markdown-body data-hash=1wdn314><p>Работал я как‑то над fine‑tuning для одной модели. Потенциала моих GPU в домашней лаборатории (home lab) для такой задачи было недостаточно. Сам fine‑tuning VLM это вообще отдельный разговор, и вопрос его целесообразности стоит обсудить отдельно, но для него мне потребовалась внешняя видеокарта.<p><strong>Я расскажу ниже, как подключить GPU от стороннего провайдера себе в Google Collab.</strong><p>На рынке сейчас есть три вида игроков:<ul><li>облака (в РФ это, например, компания‑такси, банк и социальная сеть);<li>специализированные хостинг‑провайдеры;<li>P2P‑маркетплейсы, где любой владелец видеокарты может сдать её в аренду.</ul><p>Облака - очень дорогое решение и уже давно совсем не недружелюбны с точки зрения UX. Любой специализированный хостинг‑провайдер для GPU, каким бы посредственным дизайном он ни обладал, всегда будет более удобен в использовании. Облака давно создали целый зверинец сервисов, разных правил и усложнений. И хотя они во многом похожи, зачем‑то соревнуются в том, у кого будет более интересный UI. Помимо перегруженности интерфейса, облака имеют абсолютно оборзевшие тарифы на услуги.<p>Маркетеплейсов p2p в РФ нету. Ждем когда Дуров уже запустит сервис Cocoon, вроде собирался.<p>Поэтому мой выбор обычно падает на один иностранный p2p (<a href=https://app.hyperbolic.ai/invite/xM12oRwBf>hyperbolic</a>) или на пару наших хостинг провайдеров (к примеру, <a href=https://immers.cloud/signup/r/20240122-5902662-247/>immers.cloud</a> — но это не важно для данной статьи и не является рекомендацией).<p>Кстати нормальная цена для H100 2$ и для H200 2.5$ на февраль 2026 года. Это как бы говорит о том, что, если вдруг решите обучить свою LLM, которая попала бы в топ‑100 по бенчмаркам, потребуется порядка 2 миллионов долларов.<p>Моя цель — запустить Jupyter Notebook на такой машине и пробросить порт Jupyter‑сервера на мой localhost:8888 (или на другой порт). Затем, когда мы сможем работать в этом Jupyter через <a href=http://localhost:8888>http://localhost:8888</a>, останется в интерфейсе Google Colab в настройках runtime (там, где кнопка Connect) указать Connect to a local Runtime и прописать путь к Jupyter с нужным токеном. В процессе будет несколько нюансов, но концепция проще некуда.<p>Через UI хостинг провайдера GPU нам нужно получить:<ul><li>ip<li>username<li>.pem / private key</ul><div class="markdown-alert markdown-alert-warning"><p class=markdown-alert-title><svg class="octicon octicon-alert mr-2" viewbox="0 0 16 16" aria-hidden=true height=16 width=16><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Warning<p>Образ нужно использовать Debian и с уже установленной CUDA</div><p>Для работы на удалённой машине мы даже не будем заходить на неё вручную — я буду использовать OpenTofu. Это инструмент, который полноценно заменяет Terraform от HashiCorp.<p>Для установки OpenTofu, я использую mise и вам советую.<pre><code class="hljs language-sh">mise use --global opentofu
</code></pre><p>А в моем коде, который можно <a href=https://zatsepin.dev/assets/1uwugsvi2kllazh>скачать по ссылке</a>, opentofu будет прописан в mise.toml и при установленном <a href=http://mise.jdx.dev/>mise</a>, вам даже запускать эту команду не надо будет, а tofu будет доступен при переходе в папку с кодом. Нужно выполнить <code>mise trust</code> в папке, чтобы файл mise.toml стал доверенным.<details><summary>Начнем с main.tf</summary> <pre><code class="hljs language-tf">resource <span class=hljs-string>"random_password"</span> <span class=hljs-string>"jupyter_token"</span> {
    length  = <span class=hljs-number>32</span>
    special = <span class=hljs-literal>false</span>
    override_special = <span class=hljs-string>""</span>
  }

resource <span class=hljs-string>"null_resource"</span> <span class=hljs-string>"jupyter"</span> {
  connection {
    type        = <span class=hljs-string>"ssh"</span>
    host        = <span class=hljs-keyword>var</span>.server_ip
    user        = <span class=hljs-keyword>var</span>.username
    private_key = file(<span class=hljs-keyword>var</span>.ssh_key_path)
    timeout     = <span class=hljs-string>"5m"</span>
  }

  provisioner <span class=hljs-string>"file"</span> {
    content = &lt;&lt;-EOF
      [<span class=hljs-built_in>Unit</span>]
      Description=Jupyter Notebook Service
      After=network.target

      [Service]
      Type=simple
      User=${<span class=hljs-keyword>var</span>.username}
      WorkingDirectory=/home/${<span class=hljs-keyword>var</span>.username}/notebooks
      ExecStart=/home/${<span class=hljs-keyword>var</span>.username}/.jupyter-env/bin/jupyter-notebook --no-browser --ip=<span class=hljs-number>0.0</span><span class=hljs-number>.0</span><span class=hljs-number>.0</span> --port=${<span class=hljs-keyword>var</span>.jupyter_port} --notebook-dir=/home/${<span class=hljs-keyword>var</span>.username}/notebooks --NotebookApp.token=${random_password.jupyter_token.result} --ServerApp.allow_origin=<span class=hljs-string>'https://colab.research.google.com'</span> --ServerApp.port_retries=<span class=hljs-number>0</span>
      Restart=always
      RestartSec=<span class=hljs-number>10</span>

      [Install]
      WantedBy=multi-user.target
    EOF
    destination = <span class=hljs-string>"/tmp/jupyter.service"</span>
  }

  provisioner <span class=hljs-string>"remote-exec"</span> {
    <span class=hljs-keyword>inline</span> = [
      <span class=hljs-string>"curl -LsSf https://astral.sh/uv/install.sh | sh"</span>,
      <span class=hljs-string>"export PATH=\"<span class=hljs-variable>$HOME</span>/.local/bin:<span class=hljs-variable>$PATH</span>\""</span>,
      <span class=hljs-string>"mkdir -p ~/.jupyter ~/notebooks"</span>,
      <span class=hljs-string>"uv venv --clear ~/.jupyter-env"</span>,
      <span class=hljs-string>"uv pip install --python ~/.jupyter-env/bin/python notebook torch numpy"</span>,
      
      <span class=hljs-string>"sudo mv /tmp/jupyter.service /etc/systemd/system/jupyter.service"</span>,
      <span class=hljs-string>"sudo systemctl daemon-reload"</span>,
      <span class=hljs-string>"sudo systemctl enable jupyter"</span>,
      <span class=hljs-string>"sudo systemctl restart jupyter"</span>,
      <span class=hljs-string>"sudo systemctl status jupyter --no-pager"</span>,
    ]
  }
}

resource <span class=hljs-string>"null_resource"</span> <span class=hljs-string>"configure_firewall"</span> {
  depends_on = [null_resource.jupyter]

  triggers = {
    jupyter_port = <span class=hljs-keyword>var</span>.jupyter_port
  }

  connection {
    type        = <span class=hljs-string>"ssh"</span>
    host        = <span class=hljs-keyword>var</span>.server_ip
    user        = <span class=hljs-keyword>var</span>.username
    private_key = file(<span class=hljs-keyword>var</span>.ssh_key_path)
    timeout     = <span class=hljs-string>"5m"</span>
  }

  provisioner <span class=hljs-string>"remote-exec"</span> {
    <span class=hljs-keyword>inline</span> = [
      <span class=hljs-string>"sudo apt-get update -qq"</span>,
      <span class=hljs-string>"sudo apt-get install -y ufw fail2ban"</span>,
      <span class=hljs-string>"sudo ufw --force reset"</span>,
      <span class=hljs-string>"sudo ufw allow 22/tcp"</span>,
      <span class=hljs-string>"sudo ufw default deny incoming"</span>,
      <span class=hljs-string>"sudo ufw default allow outgoing"</span>,
      <span class=hljs-string>"sudo ufw --force enable"</span>,
      <span class=hljs-string>"sudo ufw status verbose"</span>,
      <span class=hljs-string>"sudo systemctl enable fail2ban"</span>,
      <span class=hljs-string>"sudo systemctl restart fail2ban"</span>
    ]
  }
}

output <span class=hljs-string>"jupyter_instruction"</span> {
  description = <span class=hljs-string>"""
    You should do port forwarding:
    ssh -i <span class=hljs-subst>${var.ssh_key_path}</span> <span class=hljs-subst>${var.username}</span>@<span class=hljs-subst>${var.server_ip}</span> -L <span class=hljs-subst>${var.jupyter_port}</span>:localhost:<span class=hljs-subst>${var.jupyter_port}</span>"""</span>
  value = nonsensitive(<span class=hljs-string>"http://<span class=hljs-subst>${var.server_ip}</span>:<span class=hljs-subst>${var.jupyter_port}</span>/?token=<span class=hljs-subst>${random_password.jupyter_token.result}</span>"</span>)
}
</code></pre></details><p><strong>Важно для нашей задачи — скрипт запуска Jupyter:</strong><pre><code class="hljs language-sh">/home/<span class=hljs-variable>${var.username}</span>/.jupyter-env/bin/jupyter-notebook --no-browser --ip=0.0.0.0 --port=<span class=hljs-variable>${var.jupyter_port}</span> --notebook-dir=/home/<span class=hljs-variable>${var.username}</span>/notebooks --NotebookApp.token=<span class=hljs-variable>${random_password.jupyter_token.result}</span> --ServerApp.allow_origin=<span class=hljs-string>'https://colab.research.google.com'</span> --ServerApp.port_retries=0
</code></pre><p>Мы должны прописать allow_origin для google collab. В остальном скрипт не претендует на что‑то большее, чем просто запуск сервиса Jupyter на Debian. Мы открываем только SSH‑порт и блокируем попытки подключения по SSH с неправильным ключом по IP. Вся прелесть в том, что один раз написанный скрипт OpenTofu даёт возможность поднять runtime для Google Colab в любой момент — быстро и без заморочек.<div class="markdown-alert markdown-alert-tip"><p class=markdown-alert-title><svg class="octicon octicon-light-bulb mr-2" viewbox="0 0 16 16" aria-hidden=true height=16 width=16><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>Tip<p>Так как нам нужен Terraform Registry, если вы проживаете в РФ, следует использовать registry mirror:</div><p>Либо от нашего оператора такси, либо от банка:<pre><code class="hljs language-hcl">provider_installation {
  network_mirror {
	<span class=hljs-meta># https:<span class=hljs-comment>//terraform.cloud.ru/</span></span>
    url = <span class=hljs-string>"https://terraform-mirror.yandexcloud.net/"</span>
    include = [<span class=hljs-string>"registry.terraform.io/*/*"</span>]
  }
  direct {
    exclude = [<span class=hljs-string>"registry.terraform.io/*/*"</span>]
  }
}
</code></pre><h3 id=запускаем-gpu-runtime>Запускаем GPU Runtime</h3><p>Как проходит запуск инфраструктуры:<ul><li>Перешли в папку с кодом инфраструктуры <img alt src=/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260225145436_09png6z4hg.png> <ul><li>Благодаря mise инструмент tofu автоматически подвязывается в PATH — то есть команда tofu становится доступна в консоли.</ul><li>Обновим terraform.tfvars или создадим его</ul><pre><code class="hljs language-tfvars"><span class=hljs-attr>server_ip</span>      = <span class=hljs-string>"ip address"</span>
<span class=hljs-attr>username</span>       = <span class=hljs-string>"ubuntu"</span>
<span class=hljs-attr>ssh_key_path</span>   = <span class=hljs-string>"private.pem"</span>
<span class=hljs-attr>jupyter_port</span>   = <span class=hljs-number>8899</span>
</code></pre><ul><li>Смотрим доступные команды: <code>mise tasks</code><li>Инициализируем tofu <code>mise run init</code><li>Применяем конфигурацию на удалённом GPU‑сервере: <code>mise run apply</code></ul><p>Ждем...<p>В ответе получим строчку c localhost и token, которая если бы мы были на машине с нашей GPU, то она открыла бы Jupyter Notebook. Но мы у себя на машине, а GPU сервер имеет только один открытый порт и это ssh.<p><strong>Нам осталось пробросить порт на локальную машину:</strong><pre><code class="hljs language-ssh">ssh -<span class=hljs-selector-tag>i</span> private<span class=hljs-selector-class>.pem</span> ubuntu<span class=hljs-keyword>@xxx</span>.xxx.xxx.xxx -L <span class=hljs-number>8888</span>:<span class=hljs-attribute>localhost</span>:<span class=hljs-number>8888</span>
</code></pre><p>Копируем строку с токеном и пробуем открыть в браузере: <code>sh
        http://${var.server_ip}:${var.jupyter_port}/?token=${random_password.jupyter_token.result</code><p>Обе строки — для проброски порта и для доступа к Jupyter Notebook — должны быть в ответе на команду apply, так что вручную их редактировать не нужно.<p>Если в браузере всё открывается, прописываем этот сервер как local runtime в интерфейсе Google Colab: <img alt src=/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260224230358_9l29mcc3jn.png><p><strong>Попробуйте выполнить простой Python‑скрипт в Google Collab:</strong><pre><code class="hljs language-py"><span class=hljs-keyword>import</span> torch
<span class=hljs-built_in>print</span>(torch.cuda.get_device_name(<span class=hljs-number>0</span>))
</code></pre><p><img alt src=/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260225151215_5ifohtzbvu.png> Да, я запустил платный Tesla T4, хотя Google Colab предоставляет его бесплатно — но это сделано исключительно для скриншота. Стоимость использования Tesla T4 — около десяти рублей в час.<p>Полный <a href=https://zatsepin.dev/assets/1uwugsvi2kllazh>код доступен по ссылке</a> для всех, кто зарегистрировался в моём блоге.</div><script type=text/markdown>
    Работал я как‑то над fine‑tuning для одной модели. Потенциала моих GPU в домашней лаборатории (home lab) для такой задачи было недостаточно. Сам fine‑tuning VLM это вообще отдельный разговор, и вопрос его целесообразности стоит обсудить отдельно, но для него мне потребовалась внешняя видеокарта.

    **Я расскажу ниже, как подключить GPU от стороннего провайдера себе в Google Collab.**

    На рынке сейчас есть три вида игроков:

    - облака (в РФ это, например, компания‑такси, банк и социальная сеть);
    - специализированные хостинг‑провайдеры;
    - P2P‑маркетплейсы, где любой владелец видеокарты может сдать её в аренду.

    Облака - очень дорогое решение и уже давно совсем не недружелюбны с точки зрения UX. Любой специализированный хостинг‑провайдер для GPU, каким бы посредственным дизайном он ни обладал, всегда будет более удобен в использовании. Облака давно создали целый зверинец сервисов, разных правил и усложнений. И хотя они во многом похожи, зачем‑то соревнуются в том, у кого будет более интересный UI. Помимо перегруженности интерфейса, облака имеют абсолютно оборзевшие тарифы на услуги.

    Маркетеплейсов p2p в РФ нету. Ждем когда Дуров уже запустит сервис Cocoon, вроде собирался.

    Поэтому мой выбор обычно падает на один иностранный p2p ([hyperbolic](https://app.hyperbolic.ai/invite/xM12oRwBf)) или на пару наших хостинг провайдеров (к примеру, [immers.cloud](https://immers.cloud/signup/r/20240122-5902662-247/) — но это не важно для данной статьи и не является рекомендацией).

    Кстати нормальная цена для H100 2\$ и для H200 2.5\$ на февраль 2026 года. Это как бы говорит о том, что, если вдруг решите обучить свою LLM, которая попала бы в топ‑100 по бенчмаркам, потребуется порядка 2 миллионов долларов.

    Моя цель — запустить Jupyter Notebook на такой машине и пробросить порт Jupyter‑сервера на мой localhost:8888 (или на другой порт). Затем, когда мы сможем работать в этом Jupyter через http://localhost:8888, останется в интерфейсе Google Colab в настройках runtime (там, где кнопка Connect) указать Connect to a local Runtime и прописать путь к Jupyter с нужным токеном. В процессе будет несколько нюансов, но концепция проще некуда.

    Через UI хостинг провайдера GPU нам нужно получить:

    - ip
    - username
    - .pem / private key

    > [!WARNING]
    > Образ нужно использовать Debian и с уже установленной CUDA

    Для работы на удалённой машине мы даже не будем заходить на неё вручную — я буду использовать OpenTofu. Это инструмент, который полноценно заменяет Terraform от HashiCorp.

    Для установки OpenTofu, я использую mise и вам советую.

    ```sh
    mise use --global opentofu
    ```

    А в моем коде, который можно [скачать по ссылке](https://zatsepin.dev/assets/1uwugsvi2kllazh), opentofu будет прописан в mise.toml и при установленном [mise](http://mise.jdx.dev/), вам даже запускать эту команду не надо будет, а tofu будет доступен при переходе в папку с кодом. Нужно выполнить `mise trust` в папке, чтобы файл mise.toml стал доверенным.

    <details>
      <summary>Начнем с main.tf</summary>

    ```tf
    resource "random_password" "jupyter_token" {
        length  = 32
        special = false
        override_special = ""
      }

    resource "null_resource" "jupyter" {
      connection {
        type        = "ssh"
        host        = var.server_ip
        user        = var.username
        private_key = file(var.ssh_key_path)
        timeout     = "5m"
      }

      provisioner "file" {
        content = <<-EOF
          [Unit]
          Description=Jupyter Notebook Service
          After=network.target

          [Service]
          Type=simple
          User=${var.username}
          WorkingDirectory=/home/${var.username}/notebooks
          ExecStart=/home/${var.username}/.jupyter-env/bin/jupyter-notebook --no-browser --ip=0.0.0.0 --port=${var.jupyter_port} --notebook-dir=/home/${var.username}/notebooks --NotebookApp.token=${random_password.jupyter_token.result} --ServerApp.allow_origin='https://colab.research.google.com' --ServerApp.port_retries=0
          Restart=always
          RestartSec=10

          [Install]
          WantedBy=multi-user.target
        EOF
        destination = "/tmp/jupyter.service"
      }

      provisioner "remote-exec" {
        inline = [
          "curl -LsSf https://astral.sh/uv/install.sh | sh",
          "export PATH=\"$HOME/.local/bin:$PATH\"",
          "mkdir -p ~/.jupyter ~/notebooks",
          "uv venv --clear ~/.jupyter-env",
          "uv pip install --python ~/.jupyter-env/bin/python notebook torch numpy",

          "sudo mv /tmp/jupyter.service /etc/systemd/system/jupyter.service",
          "sudo systemctl daemon-reload",
          "sudo systemctl enable jupyter",
          "sudo systemctl restart jupyter",
          "sudo systemctl status jupyter --no-pager",
        ]
      }
    }

    resource "null_resource" "configure_firewall" {
      depends_on = [null_resource.jupyter]

      triggers = {
        jupyter_port = var.jupyter_port
      }

      connection {
        type        = "ssh"
        host        = var.server_ip
        user        = var.username
        private_key = file(var.ssh_key_path)
        timeout     = "5m"
      }

      provisioner "remote-exec" {
        inline = [
          "sudo apt-get update -qq",
          "sudo apt-get install -y ufw fail2ban",
          "sudo ufw --force reset",
          "sudo ufw allow 22/tcp",
          "sudo ufw default deny incoming",
          "sudo ufw default allow outgoing",
          "sudo ufw --force enable",
          "sudo ufw status verbose",
          "sudo systemctl enable fail2ban",
          "sudo systemctl restart fail2ban"
        ]
      }
    }

    output "jupyter_instruction" {
      description = """
        You should do port forwarding:
        ssh -i ${var.ssh_key_path} ${var.username}@${var.server_ip} -L ${var.jupyter_port}:localhost:${var.jupyter_port}"""
      value = nonsensitive("http://${var.server_ip}:${var.jupyter_port}/?token=${random_password.jupyter_token.result}")
    }
    ```

    </details>

    **Важно для нашей задачи — скрипт запуска Jupyter:**

    ```sh
    /home/${var.username}/.jupyter-env/bin/jupyter-notebook --no-browser --ip=0.0.0.0 --port=${var.jupyter_port} --notebook-dir=/home/${var.username}/notebooks --NotebookApp.token=${random_password.jupyter_token.result} --ServerApp.allow_origin='https://colab.research.google.com' --ServerApp.port_retries=0
    ```

    Мы должны прописать allow_origin для google collab. В остальном скрипт не претендует на что‑то большее, чем просто запуск сервиса Jupyter на Debian. Мы открываем только SSH‑порт и блокируем попытки подключения по SSH с неправильным ключом по IP.
    Вся прелесть в том, что один раз написанный скрипт OpenTofu даёт возможность поднять runtime для Google Colab в любой момент — быстро и без заморочек.

    > [!TIP]
    > Так как нам нужен Terraform Registry, если вы проживаете в РФ, следует использовать registry mirror:

    Либо от нашего оператора такси, либо от банка:

    ```hcl
    provider_installation {
      network_mirror {
    	# https://terraform.cloud.ru/
        url = "https://terraform-mirror.yandexcloud.net/"
        include = ["registry.terraform.io/*/*"]
      }
      direct {
        exclude = ["registry.terraform.io/*/*"]
      }
    }
    ```

    ### Запускаем GPU Runtime

    Как проходит запуск инфраструктуры:

    - Перешли в папку с кодом инфраструктуры
      ![](/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260225145436_09png6z4hg.png)
      - Благодаря mise инструмент tofu автоматически подвязывается в PATH — то есть команда tofu становится доступна в консоли.
    - Обновим terraform.tfvars или создадим его

    ```tfvars
    server_ip      = "ip address"
    username       = "ubuntu"
    ssh_key_path   = "private.pem"
    jupyter_port   = 8899
    ```

    - Смотрим доступные команды: `mise tasks`
    - Инициализируем tofu `mise run init`
    - Применяем конфигурацию на удалённом GPU‑сервере: `mise run apply`

    Ждем...

    В ответе получим строчку c localhost и token, которая если бы мы были на машине с нашей GPU, то она открыла бы Jupyter Notebook. Но мы у себя на машине, а GPU сервер имеет только один открытый порт и это ssh.

    **Нам осталось пробросить порт на локальную машину:**

    ```ssh
    ssh -i private.pem ubuntu@xxx.xxx.xxx.xxx -L 8888:localhost:8888
    ```

    Копируем строку с токеном и пробуем открыть в браузере:
    `sh http://${var.server_ip}:${var.jupyter_port}/?token=${random_password.jupyter_token.result`

    Обе строки — для проброски порта и для доступа к Jupyter Notebook — должны быть в ответе на команду apply, так что вручную их редактировать не нужно.

    Если в браузере всё открывается, прописываем этот сервер как local runtime в интерфейсе Google Colab:
    ![](/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260224230358_9l29mcc3jn.png)

    **Попробуйте выполнить простой Python‑скрипт в Google Collab:**

    ```py
    import torch
    print(torch.cuda.get_device_name(0))
    ```

    ![](/api/files/pbc_pages/0ipfipxovlxbu5m/pasted_image_20260225151215_5ifohtzbvu.png)
    Да, я запустил платный Tesla T4, хотя Google Colab предоставляет его бесплатно — но это сделано исключительно для скриншота. Стоимость использования Tesla T4 — около десяти рублей в час.

    Полный [код доступен по ссылке](https://zatsepin.dev/assets/1uwugsvi2kllazh) для всех, кто зарегистрировался в моём блоге.
  </script></article>]]></content:encoded></item><item><title>JJ: Почему Git устарел для разработки с помощью AI</title><link>https://zatsepin.dev/essays/lz7ogvv1zowmcqw?lang=ru</link><guid isPermaLink="true">https://zatsepin.dev/essays/lz7ogvv1zowmcqw?lang=ru</guid><pubDate>Wed, 18 Feb 2026 20:58:01 GMT</pubDate><description><![CDATA[JJ: Почему Git устарел для разработки с помощью AI — Введение в Jujutsu (JJ), систему управления версиями поверх Git, разработанную для решения проблем удобства Git и обеспечения лучшей защиты от ошибок AI-агентов. Охватывает основные концепции: операции undo/redo, управление коммитами, с практическими примерами разделения, объединения и редактирования коммитов, сохраняя полную совместимость с Git.]]></description><content:encoded><![CDATA[<article class="prose max-w-none prose-stone mx-auto drop-cap"><div class=markdown-styles data-hash=1pd92os><style>:host {
        display: block;
        position: relative;
        contain: content;
      }
      :host([hidden]) {
        display: none;
      }</style><link href=https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css media=(prefers-color-scheme:dark) rel=stylesheet><link href=https://cdn.jsdelivr.net/npm/katex@0/dist/katex.min.css rel=stylesheet></div><div class=markdown-body data-hash=12urflr><p>Представьте: ваш Claude Code или Cursor AI-агент удаляет файл или начинает переписывать его заново. Содержимое файла еще не было сохранено в git commit—оно все еще находится в unstaged состоянии. Все потеряно! Стоит только надеяться, что AI-агент сможет что-то восстановить из памяти и контрольных точек, но это костыль, может и не восстановит, а может вы пропустили этот момент.<p>Когда я переключился с чистого Git на JJ поверх Git, таких проблем и возникнуть не может. Я стал намного спокойнее относиться к своему коду при работе с AI-агентами. Позвольте мне показать, почему JJ улучшит вашу жизнь как программиста и как интегрировать его в вашу практику, не нарушая процессы в команде.<h2 id=почему-git-не-справляется-с-параллельной-разработкой-с-ии>Почему Git не справляется с параллельной разработкой с ИИ</h2><p>Git фундаментально устарел для систем, в которых код пишется параллельно с вами на вашем ПК.<p>Парадигма Git проста: вы собираете какие-то изменения локально, кладете их в ящик, ставите на этот ящик хеш-идентификатор и отправляете через конвейер ящиков. Этот ящик вам кажется неизменяемым—лучше его больше не трогать, разве что читать, что в нем сделано. Если что-то произошло, пока вы не собрали ящик, ну напишите еще раз. Обычно вы не ушли далеко, и что может произойти?<p>В терминах видеоигр, git commit это такие сохранения, которые вы делаете руками. Но вот автосохранений у вас нет.<p>Годы эта концепция была практически идеальной. Хотя надо признать, интерфейс всех возможностей Git всегда был довольно сложным. Борьба с тем, как выглядит история коммитов в разных компаниях и по сей день говорит, что в этой системе что-то не совсем хорошо.<p><strong>JJ (Jujutsu)</strong> призвана исправить проблемы удобства Git (UX), сохраняя полную совместимость с существующими репозиториями и рабочими процессами.<p>JJ—это система управления версиями, которая находится поверх Git и предоставляет гораздо более интуитивный интерфейс и богатые возможности для управления историей вашего кода, особенно при работе с AI-агентами.<p>Позвольте мне показать вам три ключевые концепции, которые делают JJ мощным инструментом:<ul><li>JJ log операции: undo и redo<li>Рабочая копия как коммит<li>Продвинутые операции: squash, split, edit</ul><h3 id=часть-1-операции-jj-с-undoredo>Часть 1: Операции JJ с Undo/Redo</h3><p>Поехали, создадим ваш первый JJ-проект:<pre><code class="hljs language-sh"><span class=hljs-built_in>mkdir</span> jj-step-0 && <span class=hljs-built_in>cd</span> jj-step-0 && jj git init
<span class=hljs-built_in>touch</span> README.md
<span class=hljs-built_in>echo</span> <span class=hljs-string>"first try"</span> >> README.md
jj diff
</code></pre><details><summary>Screenshot (jj diff)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/first_try_jj_diff_vlpobuic1g.png></p></details><p>Если выполнить команду <code>jj log</code>, то будет видно, что у вас уже есть Change ID и Commit ID. Слева — Change ID, а справа — Commit ID.<p>Change ID — это постоянный идентификатор логической задачи, над которой вы работаете. В отличие от Git-хеша (который в jj называется Commit ID), Change ID не меняется, когда вы редактируете файлы, меняете описание и т.п. Change ID никогда не меняется — даже если вы делаете <code>amend</code>, <code>rebase</code> или редактируете сообщение коммита. Основные команды <code>jj edit</code>, <code>jj describe</code> выполняются для выбранной Change ID.<p>Change ID это случайное число длиной 16 байт, которое представлено цепочкой из 12 символов от z-k. Это делает его вид уникальным и позволяет легко читать.<p>В jj рабочая директория — это всегда «живой» коммит. Любое изменение в файлах мгновенно становится частью текущей ревизии. Рабочая копия помечается символом <code>@</code> в выводе <code>jj log</code>. Hash коммита будет меняться при каждом внесении правок в проекте. По этому он не так интересен в процессе работы.<div class="markdown-alert markdown-alert-tip"><p class=markdown-alert-title><svg class="octicon octicon-light-bulb mr-2" viewbox="0 0 16 16" aria-hidden=true height=16 width=16><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>Tip<ul><li>Добавить описание к этому коммиту (через jj describe).<li>Создать следующий пустой коммит сверху (jj new), чтобы «закрыть» текущий.<li>Посмотреть статус файлов, чтобы убедиться, что всё отслеживается (jj status).</ul></div><pre><code class="hljs language-sh">jj commit -m <span class=hljs-string>"First"</span>
jj
</code></pre><details><summary>Screenshot (jj first commit)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/jj_first_commit_28kazk42lv.png></p></details><p>Наш первый коммит. 🎉🎉🎉 И на данный момент нет никаких изменений в новом рабочем, пора начать писать фичу с помощью AI-агента.<pre><code class="hljs language-sh"><span class=hljs-built_in>echo</span> <span class=hljs-string>"Cool staff"</span> >> README.md
<span class=hljs-built_in>touch</span> new-file.md && <span class=hljs-built_in>echo</span> <span class=hljs-string>"One more thing"</span> >> new-file.md
jj diff
</code></pre><details><summary>Screenshot (jj diff with changes)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/cool_staff_one_more_thing_juljg6ooid.png></p></details><p>Случается страшное: ваш AI-агент решает переписать new-file.md с нуля и удаляет оригинальное содержимое, заменяя его чем-то странным.<p>Давайте сымитируем то, что сделал AI:<pre><code class="hljs language-sh"><span class=hljs-built_in>rm</span> new-file.md
<span class=hljs-built_in>touch</span> new-file.md
<span class=hljs-built_in>echo</span> <span class=hljs-string>"Bad staff"</span> >> new-file.md
</code></pre><details><summary>Screenshot (bad changes by AI)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/bad_staff_by_ai_kdnpl9b48r.png></p></details><p>Что делать!?? Точно не будем унижаться перед ИИ и просить его восстановить нашу работу. Вместо этого давайте посмотрим, что сохранил JJ:<pre><code class="hljs language-sh">jj evolog --color=always -p
</code></pre><details><summary>Screenshot (jj evolog)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/evolog_jj_q2n79dmph9.png></p></details><p>Отлично! JJ сохраняет историю всех изменений в рабочем коммите (тут нету unstaged), а также историю всех операций, которые вы совершили. Каждое изменение, удаление или добавление—это отдельная операция с собственным хешем. Теперь давайте откатим то, что сделал AI:<pre><code class="hljs language-sh">jj undo
jj diff
</code></pre><details><summary>Screenshot (jj undo)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/undo_jj_3fqbfdc06o.png></p></details><p>Дело сделано. 🎉🎉🎉 И наш "Cool staff" на месте. Если мы вдруг осознаем, что AI был прав, мы можем вернуться с помощью:<pre><code class="hljs language-sh">jj redo
</code></pre><blockquote><p><strong>Важно</strong>: <code>undo</code> и <code>redo</code> работают с операциями, а не с изменениями файлов. Если вы сделали коммит и поняли, что нужно добавить еще что-то, просто используйте <code>jj undo</code>.</blockquote><h4 id=ключевые-команды-jj-для-операций>Ключевые команды JJ для операций</h4><table><thead><tr><th>Задача<th>Команда<tbody><tr><td>Посмотреть историю изменений<td><code>jj evolog -p</code><tr><td>Отменить последнюю операцию<td><code>jj undo</code><tr><td>Повторить после undo<td><code>jj redo</code><tr><td>Просмотреть историю операций<td><code>jj op log</code><tr><td>Восстановить конкретное состояние<td><code>jj op restore &lt;operation-id></code><tr><td>Сравнить две версии операций<td><code>jj op diff --from &lt;a> --to &lt;b> --git</code></table><h3 id=часть-2-управление-коммитами-и-историей>Часть 2: Управление коммитами и историей</h3><p>Теперь давайте разберем, как JJ упрощает управление историей коммитов и их описаниями. Начнем с нового проекта:<pre><code class="hljs language-sh"><span class=hljs-built_in>cd</span> .. && <span class=hljs-built_in>mkdir</span> jj-step-1 && <span class=hljs-built_in>cd</span> jj-step-1 && jj git init
</code></pre><p>Будем работать над тремя фичами в проекте:<pre><code class="hljs language-sh"><span class=hljs-built_in>touch</span> file.md
<span class=hljs-built_in>echo</span> <span class=hljs-string>"Feat 1"</span> >> file.md
<span class=hljs-built_in>echo</span> <span class=hljs-string>"Feature 2"</span> >> file.md
jj commit -m <span class=hljs-string>"Feat 1 & 2"</span>

<span class=hljs-built_in>touch</span> file2.md
<span class=hljs-built_in>echo</span> <span class=hljs-string>"Feature 3"</span> >> file2.md
jj commit -m <span class=hljs-string>"Feat 3"</span>
</code></pre><details><summary>Screenshot (feat 3)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/feat_3_rqscdaqsxo.png></p></details><p>Погодите, вы понимаете, что везде нужно писать "Feature", а не "Feat" в вашем коде. Так гласят новые правила в компании, вы что пропустили?. Нам надо вернуться и исправить первый коммит. С JJ это будет невероятно просто.<pre><code class="hljs language-sh">jj edit r
</code></pre><details><summary>Screenshot (jj edit r)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/edit_r_sqojhoqcr3.png></p></details><p>Обратите внимание, что мы используем только первую букву <code>r</code> вместо полного значения change ID. JJ использует "минимально достаточные" ссылки на коммиты—ровно столько символов, чтобы однозначно идентифицировать ревизию без коллизий в вашем текущем проекте. По мере роста вашего проекта потребуется больше символов.<p>Теперь мы "внутри" первого коммита. Мы можем изменить file.md как нужно, а потом изменить сообщение коммита с помощью:<p>Теперь мы "внутри" первого коммита. Мы можем изменить file.md как нужно, а потом изменить сообщение коммита с помощью:<pre><code class="hljs language-sh">jj describe
</code></pre><details><summary>Screenshot (jj describe)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/jj_describe_j1fs5fjmfo.png></p></details><h4 id=разделение-коммитов>Разделение коммитов</h4><p>Раз уж мы здесь, давайте разделим первый коммит на два отдельных коммита—по одному для каждой фичи. Ну правдо же, не хорошо делать разные задачи в одном коммите. Это также невероятно просто с помощью:<pre><code class="hljs language-sh">jj <span class=hljs-built_in>split</span>
</code></pre><p>Нажмите 'F', чтобы развернуть все изменения, выберите "Feature 1" и подтвердите с 'c'. Установите сообщение коммита "Feature 1", сохраните и установите другое сообщение для второго коммита. Теперь у вас должна быть чистая история.<details><summary>Screenshot (jj split)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/jj_split_ydc0pevu7q.png></p></details><details><summary>Screenshot (jj split success)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/jj_split_success_z9rh7vsqkv.png></p></details><p>Вернемся к нашему последнему коммиту с третьей фичей:<pre><code class="hljs language-sh">jj edit u
</code></pre><p>И начнем работать над четвертой фичей:<pre><code class="hljs language-sh">jj new -m <span class=hljs-string>"WIP 4 feat"</span>
<span class=hljs-built_in>touch</span> README.md && <span class=hljs-built_in>echo</span> <span class=hljs-string>"Blazing fast project"</span> >> README.md
</code></pre><p>В JJ есть концепция, по которой можно не использовать команду commit. Можно начинать работу с того, что сохраняется старый коммит с описанием и вы начинаете работу над чем то новым с описанием "WIP 4 feat". Потом в любой момент можно переназначить описание коммита и начать новый. Это концепция явно говорит вам, над чем вы начали работу и что вы сейчас делаете.<p>Погодите—README.md файл, который мы только что добавили, должен быть частью первого коммита. Непорядок! Но никакой проблемы, просто объедините вашу текущую работу с первым коммитом и продолжайте:<pre><code class="hljs language-sh">jj squash --to r
</code></pre><details><summary>Screenshot (jj squash)</summary> <p><img alt src=/api/files/pbc_pages/lz7ogvv1zowmcqw/jj_squash_74lwzuhimq.png></p></details><p>Отредактируйте сообщение коммита и все готово!<hr><h2 id=заключение>Заключение</h2><p>Мы попрактиковались с JJ и изучили его мощный и удобный интерфейс для Git. Красота в том, что <strong>вы можете использовать его, не изменяя процессы вашей команды</strong>—JJ работает поверх обычного Git.<p>Вы можете поддерживать более чистую историю и намного меньше беспокоиться о том, что AI случайно повредит вашу локальную работу. Уверенность, которую это приносит, бесценна в нашем тревожном мире.<p>Конечно, я не могу обещать бесшовный переход. Уверенное владение такими инструментами, как JJ, занимает больше времени, и вы можете столкнуться с моментами, когда лучше бы у вас был чистый git. Честно говоря, я принял JJ только со второй попытки использования. Как попробовать первый раз оливки, это может вам не понравиться, но если будете продолжать, то все совсем неплохо.<p>Я не затронул всё, что может делать JJ, но надеюсь, это было полезно и что вы почувствуете вдохновение попробовать его и сделать вашу работу ещё лучше.<hr><h2 id=подписывайтесь>Подписывайтесь</h2><p>Подписывайтесь на мой канал на <a href=https://zatsepin.dev/subscribe>https://zatsepin.dev/subscribe</a>, чтобы получить доступ к моему закрытому контенту, исходному коду проектов.<h2 id=референсы>Референсы</h2><ul><li><a href=https://docs.jj-vcs.dev/>https://docs.jj-vcs.dev/</a><li><a href=https://jj-for-everyone.github.io/>https://jj-for-everyone.github.io/</a><li><a href=https://steveklabnik.github.io/jujutsu-tutorial>https://steveklabnik.github.io/jujutsu-tutorial</a></ul></div></article>]]></content:encoded></item><item><title>Интерактивный снег на Bevy: создание эффекта следов через WGSL</title><link>https://zatsepin.dev/essays/g46wdri4o5qwt3h?lang=ru</link><guid isPermaLink="true">https://zatsepin.dev/essays/g46wdri4o5qwt3h?lang=ru</guid><pubDate>Fri, 13 Feb 2026 16:54:15 GMT</pubDate><description><![CDATA[Интерактивная 3D-демонстрация снежных следов, созданная на движке Bevy с физикой персонажа, динамическим освещением и камерой управления в стиле RTS.]]></description><content:encoded><![CDATA[]]></content:encoded></item></channel></rss>