Иногда LLMки путаются в языке, на котором они отвечают (П維чему нейро考ети дел思ют так). Так почему бы не помочь им забыть про языки, в которых мы не заинтересованы.

Как мы знаем, LLMки работают с токенами: переводят куски слов в многомерные вектора, много раз перемножают их на свои веса, складывают друг с другом, а потом по последнему вектору, вобравшему в себя весь контекст, вычисляют следующий токен. Соответственно, не должно быть затруднительным запретить вычислять конкретные токены из языков, которые нам не нужны.
Давайте попробуем препарировать маленькую модельку Qwen3.5-0.8B-Q8_0.gguf и поправить её.

import gguf reader = gguf.GGUFReader("./Qwen3.5-0.8B-Q8_0.gguf") tokens = reader.fields.get("tokenizer.ggml.tokens").contents() print(f"Total tokens {len(tokens)}") types = reader.fields.get("tokenizer.ggml.token_type").contents() print(f"Total token types {len(set(types))} : {set(types)}") # Total tokens 248320 # Total token types 4 : {1, 3, 4, 5}
Она использует byte-pair encoding токенизатор со словарём в 248320 токенов, разделённым на 4 типа (1 - текст, 3 - <|спец|><|токены|>, 4 - <спец></токены>, 5 - паддинг). Текстовые токены начинаются с id_0=! и доходят до id_248043=ありますか . Давайте определим список токенов, которые мы хотим оставить:
import sys keep_check = regex.compile(r"[" r"[\p{N}\p{P}\p{S}&&\p{Script=Common}]" # Числа, знаки препинания и символы r"\s" # Пробельные символы r"а-яА-ЯёЁ" # Кириллица r"a-zA-Z" # Латинский алфавит r"]", regex.V1) symbols_to_keep = set() for cp in range(sys.maxunicode + 1): if keep_check.fullmatch(chr(cp)): symbols_to_keep.add(chr(cp)) print(f"{len(symbols_to_keep)} symbols") # 7994 symbols
Так у нас появился список из 7994 символов. Теперь можно отфильтровать токены LLMки, чтобы они содержали только эти символы.
from gguf.vocab import bytes_to_unicode byte_decoder = {ch: b for b, ch in bytes_to_unicode().items()} tokens_to_rm = set() for i, t in enumerate(tokens): if types[i] != 1: # Нас интересуют только текстовые токены continue raw = bytes(byte_decoder[ch] for ch in t) try: text = raw.decode("utf-8") if not all((ch in symbols_to_keep) for ch in text): tokens_to_rm.add(i) except UnicodeDecodeError: # Обломки utf-8, пусть будут continue print(f"Remove {len(tokens_to_rm)} tokens") # Remove 100468 tokens
Итого от изначальных 248320 токенов мы удалим 100468. Будем это делать зануляя веса в предпоследнем слое, который занимается декодированием многомерного вектора в logit токена.
for t in reader.tensors: print(t.name) # output_norm.weight # token_embd.weight # blk.0.attn_gate.weight # ... ещё 329 слоёв # blk.24.nextn.shared_head_norm.weight # blk.24.post_attention_norm.weight
Вот этот token_embd.weight нам и нужен. Это общий encode/decode слой, поэтому зануление токенов отразится и на умении LLMки читать тексты на этих языках. Ну что ж, это жертва, на которую я готов пойти.
import numpy as np token_embd = next(t for t in reader.tensors if t.name == "token_embd.weight") n_vocab, bytes_per_row = token_embd.data.shape block_size = 1*2 + 32*1 # Q8_0: один fp16 масштабный коэффициент + 32 int8 n_blocks = bytes_per_row // block_size # будем патчить файл напрямую token_embd_data = np.memmap( gguf_file, dtype=np.uint8, mode="r+", offset=token_embd.data_offset, shape=(n_vocab, n_blocks, block_size) ) # зануляем fp16 масштабный коэффициент token_embd_data[tokens_to_rm, :, 0:2] = 0 token_embd_data.flush() del token_embd_data
Теперь попробуем, что у нас получилось. Запустим эту модель в llama.cpp и попробуем добиться от неё иероглифов.

На этом, собственно, всё.
Код целиком
import shutil gguf_file = shutil.copyfile( "./Qwen3.5-0.8B-Q8_0.gguf", "./Qwen3.5-0.8B-Q8_0.latcyr.gguf" ) import gguf reader = gguf.GGUFReader(gguf_file) tokens = reader.fields.get("tokenizer.ggml.tokens").contents() print(f"Total tokens {len(tokens)}") types = reader.fields.get("tokenizer.ggml.token_type").contents() print(f"Total token types {len(set(types))} : {set(types)}") import sys import regex keep_check = regex.compile(r"[" r"[\p{N}\p{P}\p{S}&&\p{Script=Common}]" # Общие числа, знаки препинания и символы r"\s" # Пробельные символы r"а-яА-ЯёЁ" # Кириллица r"a-zA-Z" # Латинский алфавит r"]", regex.V1) symbols_to_keep = set() for cp in range(sys.maxunicode + 1): if keep_check.fullmatch(chr(cp)): symbols_to_keep.add(chr(cp)) print(f"{len(symbols_to_keep)} symbols") from gguf.vocab import bytes_to_unicode BYTE_DECODER = {ch: b for b, ch in bytes_to_unicode().items()} tokens_to_rm = [] for i, t in enumerate(tokens): if types[i] != 1: continue raw = bytes(BYTE_DECODER[ch] for ch in t) try: text = raw.decode("utf-8") if not all((ch in symbols_to_keep) for ch in text): tokens_to_rm.append(i) except UnicodeDecodeError: # Обломки utf-8 continue print(f"Remove {len(tokens_to_rm)} tokens") import numpy as np token_embd = next(t for t in reader.tensors if t.name == "token_embd.weight") n_vocab, bytes_per_row = token_embd.data.shape block_size = 1*2 + 32*1 # Q8_0: один fp16 масштабный коэффициент + 32 int8 n_blocks = bytes_per_row // block_size token_embd_data = np.memmap( gguf_file, dtype=np.uint8, mode="r+", offset=token_embd.data_offset, shape=(n_vocab, n_blocks, block_size) ) token_embd_data[tokens_to_rm, :, 0:2] = 0 # зануляем fp16 масштабный коэффициент token_embd_data.flush() del token_embd_data
aaabramenko
Лоботомия XXI века