Pull to refresh
0
rull wull@rullwull

User

-0,6
Rating
2
Subscribers
Send message

Qwen2.5-VL на AMD

Прошлый пост про Whisper + pyannote на AMD читают, но молчат) Ладно, продолжу.

У меня две свободные машины. На 4090 живёт DeepSeek-R1-32B с LoRA-адаптерами, весь VRAM его. На AMD RX 7900 XTX крутятся Whisper + pyannote — занимают ~5 GB из 24. Свободно 19 — решил добавить Qwen2.5-VL-7B для чтения изображений: фото документов, визитки, скриншоты. В bf16 модель весит ~14 GB, должна влезть.

Поставил, запустил оба systemd-сервиса — система зависла намертво. Две модели грузятся параллельно, каждая маппит safetensors в оперативку перед VRAM — а RAM всего 15 GB. OOM killer, рестарт, снова OOM, цикл. Грузился через GRUB в текстовый режим (параметр 3, но без nomodeset — иначе amdgpu не поднимется). Отключил GUI — framebuffer ещё 1-2 GB VRAM жрал. Swap до 16 GB, последовательный запуск. Заработало.

Отправил фото страницы A4 — инструкция кондиционера, русский, два столбца. Через 398 секунд получил одну строку и бесконечный loop: "Постановление Правительства!!!!!!"!!!!!!!#!!!!!!!$...". Шесть минут на мусор. На коротких описаниях картинок модель работала нормально — 35 секунд, осмысленный текст. Но OCR документов — полный провал.

Первая мысль — bottleneck в железе. У меня опыт с CUDA→HIP конвертером, 500+ проектов, уже приготовился конвертировать flash-attention под RDNA3. Но сначала бенчмарки: attention — работает (AOTriton 1.9ms), FFN — 1.73ms, text-only генерация — 6 tok/s. Железо в порядке, flash-attention конвертировать не нужно.

Виновник — repetition_penalty=1.15. Добавил для борьбы с loop’ами, стандартная практика. На ROCm этот параметр даёт 2.3x замедление. На NVIDIA дешёвый, на AMD дорогой. Нигде не документировано. Убрал, добавил early-stop через StoppingCriteria — каждые 24 токена проверяю хвост, если loop — прерываю. Итог: 398с → 13с.

Но из целой страницы модель вытащила полтора предложения — 114 символов из 2000+. Семёрка теряет фокус на длинных документах. Сделал ресёрч — для Qwen2.5-VL критично разрешение и количество vision-токенов. Пошёл путём препроцессинга: OpenCV pipeline перед моделью (выравнивание, контраст, резкость), увеличил max_pixels в processor. Главное — tiled OCR: режу фото на 3 полосы, каждую отдельно, склеиваю. Single-pass: 114 символов. Tiled: 3077 символов, 85% покрытия. Не идеал — есть повторы на стыках, двухколонные путают — но направление правильное.

Кстати, для общения с моделями использую SimpleX CLI. На сервере Python-bridge слушает WebSocket, маршрутизирует: голосовое → Whisper, фото → Qwen-VL. С телефона отправил — через минуту ответ в чате. Если интересно — расскажу подробнее.

Что важно, если ставите vision-модель на AMD: OOM при параллельном старте - swap + задержка между сервисами. GUI отключить если VRAM впритык. local_files_only=True при загрузке модели. И repetition_penalty на ROCm — заменить на early-stop, серьёзно.

Стек: AMD RX 7900 XTX 24GB, PyTorch 2.5.1+ROCm 6.2, Qwen2.5-VL-7B bf16, Whisper + pyannote — три модели на одной карте, Ubuntu 24.04 без GUI.

Tags:
+3
Comments0

Обучение переходит в опыт: Whisper + pyannote на AMD завёлся с первого раза

Сегодня пишу именно потому, что замкнулся цикл от обучения к продукту. Тема специфическая, мало кому интересно использовать AMD для нейронок вместо NVIDIA, но раз уж прошёл через это сам поделюсь решениями. Дальше кому надо берите, экономьте себе недели.

Изначально AMD у меня появился под другую задачу. Я исследовал возможность сделать веса для CUDA→HIP конвертера. AMD-машина под столом осталась, и я наконец начал использовать её для проектов, а не только для экспериментов с конвертером.

Понадобилось сделать голосовой ассистент для анализа встреч. Нужна транскрипция русской речи с разделением говорящих. Whisper large-v3 + pyannote.audio 3.1 - стандартный стек, только обычно его гоняют на NVIDIA. Я решил ставить на AMD RX 7900 XTX (24 GB VRAM, PyTorch ROCm 6.2). DeepSeek 32B стоит на соседней машине с 4090, ему нужен весь её VRAM, а ещё на 4090 загружаю весовые LoRA-адаптеры пользователей. Завёлся с первого раза. От этого и кайфанул - обучение перешло в опыт. Можно сказать, записалось в мои веса.

Из коробки сработало всё, никаких откатов версий, никаких ручных сборок:

bash pip3 install openai-whisper --break-system-packages pip3 install pyannote.audio --no-deps --break-system-packages pip3 install omegaconf pytorch-metric-learning rich soundfile torchmetrics --break-system-packages pip3 install fastapi uvicorn python-multipart --break-system-packages ​

Обе модели занимают ~5.9 GB VRAM из 24. Whisper 3 GB, pyannote 2 GB.

Дальше самое ценное мои грабли, которые решил заранее, за предыдущие месяцы боли. На эти вещи обычно тратят недели.

Первое - pip install --no-deps для pyannote. Обычный pip install pyannote.audio тянет torch как зависимость. pip видит «torch уже установлен», но не разбирается, что у тебя специальный PyTorch ROCm build, и ставит CUDA-версию поверх. PyTorch ROCm убит, вся экосистема AMD ломается. С флагом --no-deps pip ставит pyannote без зависимостей, дальше вручную доставляешь omegaconf, pytorch-metric-learning, soundfile, torchmetrics, rich. Чисто, ничего не ломается.

Второе - API pyannote 3.1 сломали тихо. В 3.0 было result.itertracks(yield_label=True). В 3.1 - result.speaker_diarization.itertracks(yield_label=True). Документация молчит, узнаёшь через ошибку. Плюс use_auth_token переименован в token без фанфар.

​```python from pyannote.audio import Pipeline

pipeline = Pipeline.from_pretrained( “pyannote/speaker-diarization-3.1”, token=HF_TOKEN, # не use_auth_token! ) pipeline.to(torch.device(“cuda”))

result = pipeline({“waveform”: waveform, “sample_rate”: sr})

for turn, _, speaker in result.speaker_diarization.itertracks(yield_label=True): print(f"{turn.start:.2f} - {turn.end:.2f}: {speaker}") ​```

Третье - torchcodec тихая мина на ROCm. pyannote в новых версиях пытается использовать torchcodec для декодирования аудио. На AMD ROCm torchcodec не собран, падает с невнятной ошибкой про libavutil. Обход - подавать waveform напрямую через torchaudio:

​```python import torchaudio

waveform, sample_rate = torchaudio.load(audio_path) result = pipeline({“waveform”: waveform, “sample_rate”: sample_rate}) ​```

pyannote-команда упоминает эту возможность мелкими буквами в одном issue на GitHub. Работает идеально.

Четвёртое - нужна переменная окружения TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1. Без неё часть операций fallback-ит на медленный путь.

Пятое - две модели в одном процессе на ROCm работают. Была мысль разносить Whisper и pyannote по процессам - вдруг конфликты HIP runtime. Нет. Обе модели грузятся в одном Python-процессе, работают параллельно.

По производительности на 9-минутном WAV (16 kHz, два говорящих, русская речь) Whisper large-v3 отрабатывает за ~60 секунд, pyannote 3.1 за ~3 секунды, итого ~63 секунды. pyannote практически бесплатен. Качество диаризации отличное - два спикера разнесены корректно, таймкоды совпадают с репликами. Стек: PyTorch ROCm 6.2, openai-whisper, pyannote.audio 3.1. RX 7900 XTX, 24 GB VRAM, Ubuntu 24.04.

Tags:
0
Comments0

Как отключить reasoning у локального DeepSeek-R1 и не сойти с ума

Третий пост из серии про грабли локальных LLM. Первый — про микрочанки, отравляющие RAG. Второй — про embedding модель, которая не знает русский. Сейчас — про reasoning, который жрёт ресурсы и не выключается.

Проблема

DeepSeek-R1-Distill-Qwen-32B — reasoning модель. На каждый запрос она сначала «думает» в блоке <think>...</think>, потом отвечает. Выглядит так:

<think>
Хорошо, мне нужно помочь пользователю распределить задачи
для проекта создания цифрового двойника для молочной фермы.
Я новичок в этом, поэтому постараюсь разобраться шаг за шагом.

Сначала, мне нужно понять, что такое цифровой двойник...
</think>

Разработка цифрового двойника для молочной фермы — это сложный проект...

Блок <think> может быть длиннее самого ответа. Это токены, это время, это VRAM. Для задач где рассуждения не нужны — чистый оверхед.

Наивное решение — не работает

Первая идея: убрать <think> из ответа регуляркой постфактум.

response_text = re.sub(r'<think>.*?</think>', '', response_text, flags=re.DOTALL).strip()

Проблема: модель всё равно генерирует рассуждения. Вы просто прячете их от пользователя, но GPU уже потратил время и токены.

Решение от сообщества

Пустой блок <think>\n\n</think> в конце промпта. Модель видит, что фаза рассуждений уже «завершена», и сразу переходит к ответу.

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
text = text + "<think>\n\n</think>\n\n"

Попробовал — не работает. Reasoning остаётся.

Ловушка с токенизатором

Смотрю в лог что реально уходит модели:

...ть задачи?<|Assistant|><think><think>

</think>

Два <think>. Токенизатор DeepSeek при add_generation_prompt=True уже добавляет <think> в конец промпта автоматически. Мой код добавляет второй. Модель видит незакрытый первый тег и начинает думать.

Причём <|Assistant|> — это не обычные символы |, а полноширинные юникодные . Специальные токены DeepSeek. Если искать обычный | в строке — не найдёте.

Правильное решение

Проверять, что уже есть в промпте, и действовать по ситуации:

def prepare_prompt_no_thinking(messages, tokenizer):
    text = tokenizer.apply_chat_template(
        messages, 
        tokenize=False, 
        add_generation_prompt=True
    )
    
    if "<think>\n\n</think>" in text:
        pass  # Уже закрыт
    elif "<think>" in text and "</think>" not in text:
        text = text + "\n\n</think>\n\n"  # Закрываем открытый
    else:
        text = text + "<think>\n\n</think>\n\n"  # Добавляем пустой
    
    return text

Три ветки — потому что разные версии токенизатора ведут себя по-разному. Кто-то добавляет <think>, кто-то нет.

Результат

Без тегов:

Хорошо, мне нужно помочь пользователю распределить задачи
для проекта создания цифрового двойника для молочной фермы.
Я новичок в этом, поэтому постараюсь разобраться шаг за шагом...

С правильными тегами:

Разработка цифрового двойника для молочной фермы — это сложный проект,
который требует участия специалистов из разных областей.
Вот примерное распределение задач:

Модель сразу отвечает по делу, без вступительных рассуждений. Экономия токенов и времени — в зависимости от запроса от 30% до 60%.

Вывод

Если используете DeepSeek-R1-Distill локально и reasoning вам не нужен — не режьте его регуляркой постфактум. Закройте <think> тег до генерации. Но обязательно проверяйте, что токенизатор уже добавил — иначе получите дубль и потратите час на дебаг того, что должно было занять минуту.

Tags:
Total votes 3: ↑2 and ↓1+2
Comments3

Идеальная база знаний, а RAG возвращает мусор — проблема не там, где кажется

Продолжение предыдущего поста про микрочанки, где 3 мусорных документа отравили весь RAG. Тогда проблема была в данных. Сейчас — данные идеальные, а поиск всё равно не работает.

Контекст

Строю локальную мультиагентную систему. Собрал базу знаний: 85 архитектурных блоков, 160 чанков в ChromaDB, реальный опыт — не синтетика. Embedding модель — стандартная all-MiniLM-L6-v2. Документы на русском с вкраплениями английских терминов (DPO, LoRA, VRAM — как у всех).

Симптом

Спрашиваю: "DPO патч для OOM" — в базе есть целый блок про это. RAG возвращает документ про права доступа к Project Context. Вообще мимо.

Спрашиваю: "positive feedback loop" — в базе есть блок №57 ровно с таким названием. RAG его не находит, dist=0.746.

Диагностика

Подозрение — embedding модель не понимает русский текст. Проверяю: один и тот же смысл, три формулировки.

queries = [
    ("positive feedback loop", "английский"),
    ("петля положительной обратной связи", "русский"),
    ("цикл доверие данные результат", "русский контекст"),
]

for q, lang in queries:
    results = col.query(query_texts=[q], n_results=1, include=["documents", "distances"])
    dist = results['distances'][0][0]
    print(f"[{lang}] dist={dist:.3f} | '{q}'")
[английский]       dist=0.746 | 'positive feedback loop'
[русский]           dist=0.566 | 'петля положительной обратной связи'
[русский контекст]  dist=0.504 | 'цикл доверие данные результат'

Один смысл — разница в полтора раза. При этом документы в базе на русском. Английский запрос к русским документам — модель не может их сопоставить.

Почему так

all-MiniLM-L6-v2 обучалась на английских текстах. Она превращает текст в вектор из 384 чисел. Для английского — вектор осмысленный, семантически правильный. Для русского — видит буквы, но не понимает смысл. Вектор получается случайный.

Это как нанять переводчика, который знает только английский, и попросить его искать по русской библиотеке.

А может перевести всё на английский?

Первая мысль — перевести все документы на английский, запросы тоже переводить на лету, а результат обратно на русский. Английские embedding модели объективно лучше отточены, больше данных, больше бенчмарков.

Но для локальной системы с русскими документами это плохой вариант. Технические термины с контекстом теряются при переводе. Появляется двойная задержка — перевод запроса туда, результата обратно. Нужен ещё один сервис (переводчик), а задача — держать всё локально. И главное — ошибки накапливаются: плохой перевод → плохой вектор → плохой результат.

Для чисто английских доков — да, держите всё на английском. Но когда документы изначально на русском с кучей специфики — мультиязычная модель проще. Меньше движущихся частей.

Решение

Заменил all-MiniLM-L6-v2 на paraphrase-multilingual-MiniLM-L12-v2. Модель обучена на 50+ языках, включая русский. Понимает смешанный текст типа “DPO обучение на LoRA адаптере” — то, что в локальных ML-проектах встречается на каждом шагу.

Пересоздал все коллекции с новой моделью. Результат:

# Было (all-MiniLM-L6-v2):
# 'positive feedback loop' → dist=0.746, нашёл мусор

# Стало (multilingual):
# 'positive feedback loop' → dist=0.35, нашёл именно блок про Feedback Loop

Поиск заработал сразу. На все запросы — и русские, и английские, и смешанные.

Вывод

Если строите RAG на русском (или любом не-английском) — не берите all-MiniLM-L6-v2 по дефолту. Она стоит первой в каждом туториале, но для нелатинских языков это ловушка. Данные могут быть идеальными, чанкинг правильным, а поиск будет возвращать мусор — потому что “переводчик” не знает ваш язык.

Замена embedding модели на мультиязычную — одна строчка кода и пересоздание коллекций. Пять минут работы, которые сэкономят дни дебага.

# Было
ef = SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")

# Стало
ef = SentenceTransformerEmbeddingFunction(model_name="paraphrase-multilingual-MiniLM-L12-v2")
Tags:
Total votes 5: ↑4 and ↓1+3
Comments5

3 мусорных документа (1.7%) отравили весь мой RAG

Строю локальную мультиагентную систему с RAG на ChromaDB. В какой-то момент модель начала нести чушь — вставлять в ответы куски маркдауна, генерить мусор вместо нормальных ответов.

Симптом

Спрашиваю: «Новый проект: цифровой двойник нефтеперерабатывающего завода. Как декомпозировать?»

В ответе — рандомные огрызки разметки типа "25*\n*Тип: Инфраструктура и системное администрирование*". Модель явно копировала что-то из контекста.

Копаю

Смотрю что RAG возвращает на этот запрос:

results = rag.query(query_texts=[query], n_results=3)
for doc in results['documents'][0]:
    print(f"[{len(doc)} chars]: {doc[:50]}")
[56 chars]: 25*\n*Тип: Инфраструктура и системное администр...
[15 chars]:  и реализация*...
[11 chars]: ров вместе...

Топ-3 — мусор, а не документы.

Причина

При загрузке маркдаун-файлов в ChromaDB чанкер резал по 800 символов механически — посередине заголовков, посередине предложений. В итоге появились микро-огрызки типа "ров вместе" (11 символов), которые стали отдельными документами.

Почему короткие чанки ломают RAG

Короткий текст → странный эмбеддинг. Вектор ни о чём, без смысла. И именно поэтому он оказывается «близок» к любому запросу случайным образом. Мой 11-символьный огрызок стабильно обгонял нормальные 800-символьные документы в similarity search.

Фикс

Нашёл все документы меньше 100 символов:

all_docs = rag.get(include=["documents"])
short = [(i, doc) for i, doc in enumerate(all_docs['documents']) if len(doc) < 100]
print(f"Мусор: {len(short)}")  # 3

Удалил:

ids_to_delete = [all_docs['ids'][i] for i, _ in short]
rag.delete(ids=ids_to_delete)
# Было: 176 docs → Стало: 173 docs

Галлюцинации прекратились сразу.

Вывод

Фильтруйте чанки по минимальной длине до загрузки в векторную БД:

MIN_CHUNK_LENGTH = 100
chunks = [c for c in chunks if len(c) >= MIN_CHUNK_LENGTH]

3 документа из 176 — это 1.7%. Процент не имеет значения. Если у мусора странный вектор — он всплывёт. Один плохой документ может отравить весь ваш RAG.

Tags:
Total votes 6: ↑4 and ↓2+3
Comments5

Information

Rating
Does not participate
Registered
Activity

Specialization

UTXO Blockchain Protocol Developer · ML Engineer · AI Researcher
Ведущий
Python
SQL
HTML
Cuda
C++
Linux
Golang
Git
Docker
Rust