Pull to refresh
4K+
187
155
Subscribers
Send message

Тут, конечно, это уже надо на нормальном датасете обучать, чтобы заметить разницу. Но архитектура старая, поддержки triton нет, обучение будет в разы дольше, чем дообучить даже, например, Qwen3.5 4B.

Но в целом можно попробовать и так. Сходу при запуске инференса со Sparse Attn ошибка:

cuda\TensorCompare.cu:109: block: [0,0,0], thread: [0,0,0] Assertion `input[0] != 0` failed.
    next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
                  ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
torch.AcceleratorError: CUDA error: device-side assert triggered

Если посмотреть вывод модели, то вероятности обнулены, логиты в бесконечности, а маска внимания все маркирует как False. Можно попробовать починить:

modeling_rugpt3xl.py
"""PyTorch RuGPT-3 XL model.

GPT-3-style decoder-only transformer (1.3B) trained on Russian text.
Architecture: absolute position embeddings, pre-norm layers, GELU activation,
tied LM head.
"""

import math
from typing import List, Optional, Tuple, Union

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint

from transformers.activations import ACT2FN
from transformers.cache_utils import Cache, DynamicCache
from transformers.generation import GenerationMixin
from transformers.modeling_outputs import (
    BaseModelOutputWithPast,
    CausalLMOutputWithPast,
)
from transformers.modeling_utils import PreTrainedModel
from transformers.utils import logging

from .configuration_rugpt3xl import RuGPT3XLConfig

logger = logging.get_logger(__name__)


def _make_sparse_layout(
    num_heads: int,
    num_blocks: int,
    num_local_blocks: int,
    num_global_blocks: int,
    num_different_global_patterns: int,
    device: torch.device,
) -> torch.Tensor:
    """Build FixedSparsity boolean layout on *device*.

    Returns [num_heads, num_blocks, num_blocks] bool tensor.
    """
    layout = torch.zeros(
        num_heads, num_blocks, num_blocks, dtype=torch.bool, device=device,
    )

    for win in range(0, num_blocks, num_local_blocks):
        end = min(win + num_local_blocks, num_blocks)
        sz = end - win
        layout[:, win:end, win:end] = torch.tril(
            torch.ones(sz, sz, dtype=torch.bool, device=device)
        )

    for h in range(num_heads):
        first = num_local_blocks - (
            1 + h % num_different_global_patterns
        ) * num_global_blocks
        reg_end = num_blocks - (num_blocks % num_local_blocks)
        for gi in range(first, reg_end, num_local_blocks):
            layout[h, gi:, gi : gi + num_global_blocks] = True
        if reg_end < num_blocks:
            s = min(reg_end + first, num_blocks - num_global_blocks)
            layout[h, s:, s : s + num_global_blocks] = True

    return layout


class RuGPT3XLAttention(nn.Module):
    def __init__(self, config: RuGPT3XLConfig, layer_idx: int):
        super().__init__()
        self.config = config
        self.layer_idx = layer_idx
        self.hidden_size = config.hidden_size
        self.num_heads = config.num_attention_heads
        self.head_dim = self.hidden_size // self.num_heads
        self.scale = self.head_dim ** -0.5

        self.q_proj = nn.Linear(self.hidden_size, self.hidden_size)
        self.k_proj = nn.Linear(self.hidden_size, self.hidden_size)
        self.v_proj = nn.Linear(self.hidden_size, self.hidden_size)
        self.o_proj = nn.Linear(self.hidden_size, self.hidden_size)

        self.attn_dropout = nn.Dropout(config.attention_dropout)
        self.resid_dropout = nn.Dropout(config.output_dropout)

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_value: Optional[Cache] = None,
        output_attentions: bool = False,
        use_cache: bool = False,
        **kwargs,
    ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Cache]]:
        bsz, q_len, _ = hidden_states.size()

        query = self.q_proj(hidden_states)
        key = self.k_proj(hidden_states)
        value = self.v_proj(hidden_states)

        query = query.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
        key = key.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
        value = value.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)

        if past_key_value is not None:
            key, value = past_key_value.update(key, value, self.layer_idx)

        attn_weights = torch.matmul(query, key.transpose(2, 3)) * self.scale

        if attention_mask is not None:
            attn_weights = attn_weights + attention_mask

        attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(
            query.dtype
        )
        attn_weights = self.attn_dropout(attn_weights)

        attn_output = torch.matmul(attn_weights, value)
        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)

        attn_output = self.o_proj(attn_output)
        attn_output = self.resid_dropout(attn_output)

        return (
            attn_output,
            attn_weights if output_attentions else None,
            past_key_value,
        )


class RuGPT3XMLP(nn.Module):
    def __init__(self, config: RuGPT3XLConfig):
        super().__init__()
        self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size)
        self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size)
        self.act_fn = ACT2FN[config.hidden_act]
        self.dropout = nn.Dropout(config.output_dropout)

    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        return self.dropout(self.down_proj(self.act_fn(self.up_proj(hidden_states))))


class RuGPT3XLDecoderLayer(nn.Module):
    def __init__(self, config: RuGPT3XLConfig, layer_idx: int):
        super().__init__()
        self.input_layernorm = nn.LayerNorm(
            config.hidden_size, eps=config.layer_norm_eps
        )
        self.self_attn = RuGPT3XLAttention(config, layer_idx)
        self.post_attention_layernorm = nn.LayerNorm(
            config.hidden_size, eps=config.layer_norm_eps
        )
        self.mlp = RuGPT3XMLP(config)

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_value: Optional[Cache] = None,
        output_attentions: bool = False,
        use_cache: bool = False,
        **kwargs,
    ) -> Tuple[torch.Tensor, ...]:
        residual = hidden_states
        hidden_states = self.input_layernorm(hidden_states)
        hidden_states, self_attn_weights, present_key_value = self.self_attn(
            hidden_states=hidden_states,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_value=past_key_value,
            output_attentions=output_attentions,
            use_cache=use_cache,
            **kwargs,
        )
        hidden_states = residual + hidden_states

        residual = hidden_states
        hidden_states = self.post_attention_layernorm(hidden_states)
        hidden_states = self.mlp(hidden_states)
        hidden_states = residual + hidden_states

        outputs = (hidden_states,)
        if output_attentions:
            outputs += (self_attn_weights,)
        if use_cache:
            outputs += (present_key_value,)
        return outputs


class RuGPT3XLPreTrainedModel(PreTrainedModel):
    config_class = RuGPT3XLConfig
    base_model_prefix = "model"
    supports_gradient_checkpointing = True
    _no_split_modules = ["RuGPT3XLDecoderLayer"]
    _skip_keys_device_placement = ["past_key_values"]
    _supports_cache_class = True

    def _init_weights(self, module):
        std = self.config.initializer_range
        if isinstance(module, nn.Linear):
            module.weight.data.normal_(mean=0.0, std=std)
            if module.bias is not None:
                module.bias.data.zero_()
        elif isinstance(module, nn.Embedding):
            module.weight.data.normal_(mean=0.0, std=std)
            if module.padding_idx is not None:
                module.weight.data[module.padding_idx].zero_()
        elif isinstance(module, nn.LayerNorm):
            module.bias.data.zero_()
            module.weight.data.fill_(1.0)


class RuGPT3XLModel(RuGPT3XLPreTrainedModel):
    """Bare RuGPT-3 XL transformer outputting raw hidden states."""

    def __init__(self, config: RuGPT3XLConfig):
        super().__init__(config)
        self.padding_idx = config.pad_token_id
        self.vocab_size = config.vocab_size

        self.embed_tokens = nn.Embedding(
            config.vocab_size, config.hidden_size, self.padding_idx
        )
        self.embed_positions = nn.Embedding(
            config.max_position_embeddings, config.hidden_size
        )
        self.embed_dropout = nn.Dropout(config.embedding_dropout)

        self.layers = nn.ModuleList(
            [
                RuGPT3XLDecoderLayer(config, layer_idx)
                for layer_idx in range(config.num_hidden_layers)
            ]
        )
        self.norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)

        # Sparse attention config
        self._sparse_layers: set = set()
        if getattr(config, "sparse_mode", "none") == "alternating":
            self._sparse_layers = {
                i for i in range(config.num_hidden_layers) if i % 2 == 0
            }
        elif getattr(config, "sparse_mode", "none") == "all":
            self._sparse_layers = set(range(config.num_hidden_layers))

        # Sparse layout will be lazily built on first forward.
        # NOT registered as a buffer to avoid meta-device corruption.
        self._sparse_layout: Optional[torch.Tensor] = None

        self.gradient_checkpointing = False
        self.post_init()

    def _get_sparse_layout(self, device: torch.device) -> torch.Tensor:
        """Return sparse layout tensor on *device*, building it if necessary."""
        if self._sparse_layout is not None and self._sparse_layout.device == device:
            return self._sparse_layout

        cfg = self.config
        num_blocks = cfg.max_position_embeddings // cfg.sparse_block_size
        self._sparse_layout = _make_sparse_layout(
            num_heads=cfg.num_attention_heads,
            num_blocks=num_blocks,
            num_local_blocks=cfg.sparse_num_local_blocks,
            num_global_blocks=cfg.sparse_num_global_blocks,
            num_different_global_patterns=cfg.sparse_num_different_global_patterns,
            device=device,
        )
        return self._sparse_layout

    def get_input_embeddings(self):
        return self.embed_tokens

    def set_input_embeddings(self, value):
        self.embed_tokens = value

    def forward(
        self,
        input_ids: Optional[torch.LongTensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_values: Optional[Union[Cache, List[torch.FloatTensor]]] = None,
        inputs_embeds: Optional[torch.FloatTensor] = None,
        use_cache: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
        **kwargs,
    ) -> Union[Tuple, BaseModelOutputWithPast]:
        output_attentions = (
            output_attentions
            if output_attentions is not None
            else self.config.output_attentions
        )
        output_hidden_states = (
            output_hidden_states
            if output_hidden_states is not None
            else self.config.output_hidden_states
        )
        use_cache = use_cache if use_cache is not None else self.config.use_cache
        return_dict = (
            return_dict if return_dict is not None else self.config.use_return_dict
        )

        if input_ids is not None and inputs_embeds is not None:
            raise ValueError(
                "You cannot specify both input_ids and inputs_embeds"
            )
        if input_ids is not None:
            batch_size, seq_length = input_ids.shape[:2]
        elif inputs_embeds is not None:
            batch_size, seq_length = inputs_embeds.shape[:2]
        else:
            raise ValueError(
                "You have to specify either input_ids or inputs_embeds"
            )

        if self.gradient_checkpointing and self.training and use_cache:
            logger.warning_once(
                "`use_cache=True` is incompatible with gradient checkpointing. "
                "Setting `use_cache=False`."
            )
            use_cache = False

        past_key_values_length = 0
        if use_cache:
            if past_key_values is None:
                past_key_values = DynamicCache()
            past_key_values_length = past_key_values.get_seq_length()

        if position_ids is None:
            device = (
                input_ids.device if input_ids is not None else inputs_embeds.device
            )
            position_ids = torch.arange(
                past_key_values_length,
                seq_length + past_key_values_length,
                dtype=torch.long,
                device=device,
            ).unsqueeze(0)

        if inputs_embeds is None:
            inputs_embeds = self.embed_tokens(input_ids)

        position_embeds = self.embed_positions(position_ids)
        hidden_states = self.embed_dropout(inputs_embeds + position_embeds)

        # Dense causal mask
        causal_mask = self._build_causal_mask(
            batch_size,
            seq_length,
            past_key_values_length,
            hidden_states.dtype,
            hidden_states.device,
            attention_mask,
        )

        # Sparse causal mask (lazily build layout on correct device)
        sparse_mask = None
        if self._sparse_layers:
            sparse_layout = self._get_sparse_layout(hidden_states.device)
            sparse_mask = self._build_sparse_causal_mask(
                seq_length,
                past_key_values_length,
                hidden_states.dtype,
                hidden_states.device,
                sparse_layout,
                self.config.sparse_block_size,
                attention_mask,
            )

        all_hidden_states = () if output_hidden_states else None
        all_self_attns = () if output_attentions else None
        next_decoder_cache = None

        for layer_idx, decoder_layer in enumerate(self.layers):
            if output_hidden_states:
                all_hidden_states += (hidden_states,)

            layer_mask = (
                sparse_mask
                if (layer_idx in self._sparse_layers and sparse_mask is not None)
                else causal_mask
            )

            if self.gradient_checkpointing and self.training:
                layer_outputs = self._gradient_checkpointing_func(
                    decoder_layer.__call__,
                    hidden_states,
                    layer_mask,
                    position_ids,
                    past_key_values,
                    output_attentions,
                    use_cache,
                )
            else:
                layer_outputs = decoder_layer(
                    hidden_states,
                    attention_mask=layer_mask,
                    position_ids=position_ids,
                    past_key_value=past_key_values,
                    output_attentions=output_attentions,
                    use_cache=use_cache,
                )

            hidden_states = layer_outputs[0]
            if use_cache:
                next_decoder_cache = layer_outputs[
                    2 if output_attentions else 1
                ]
            if output_attentions:
                all_self_attns += (layer_outputs[1],)

        hidden_states = self.norm(hidden_states)

        if output_hidden_states:
            all_hidden_states += (hidden_states,)

        next_cache = next_decoder_cache if use_cache else None

        if not return_dict:
            return tuple(
                v
                for v in [
                    hidden_states,
                    next_cache,
                    all_hidden_states,
                    all_self_attns,
                ]
                if v is not None
            )
        return BaseModelOutputWithPast(
            last_hidden_state=hidden_states,
            past_key_values=next_cache,
            hidden_states=all_hidden_states,
            attentions=all_self_attns,
        )

    @staticmethod
    def _build_causal_mask(
        batch_size: int,
        seq_length: int,
        past_length: int,
        dtype: torch.dtype,
        device: torch.device,
        attention_mask: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        total_length = past_length + seq_length
        causal = torch.full(
            (seq_length, total_length),
            torch.finfo(dtype).min,
            device=device,
        )
        causal = causal.masked_fill(
            torch.arange(total_length, device=device).unsqueeze(0)
            <= torch.arange(
                past_length, past_length + seq_length, device=device
            ).unsqueeze(1),
            0.0,
        )
        causal = causal.unsqueeze(0).unsqueeze(0)

        if attention_mask is not None:
            pad_mask = (
                (1 - attention_mask[:, None, None, :].to(dtype))
                * torch.finfo(dtype).min
            )
            causal = causal + pad_mask

        return causal

    @staticmethod
    def _build_sparse_causal_mask(
        seq_length: int,
        past_length: int,
        dtype: torch.dtype,
        device: torch.device,
        sparse_layout: torch.Tensor,
        block_size: int,
        attention_mask: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        total_length = past_length + seq_length
        num_blocks = sparse_layout.shape[1]

        q_block = (
            torch.arange(past_length, past_length + seq_length, device=device)
            // block_size
        ).clamp(max=num_blocks - 1)
        k_block = (
            torch.arange(total_length, device=device) // block_size
        ).clamp(max=num_blocks - 1)

        block_ok = sparse_layout[:, q_block][:, :, k_block]

        q_pos = torch.arange(
            past_length, past_length + seq_length, device=device
        ).unsqueeze(1)
        k_pos = torch.arange(total_length, device=device).unsqueeze(0)
        causal_ok = k_pos <= q_pos

        allowed = block_ok & causal_ok.unsqueeze(0)

        min_val = torch.finfo(dtype).min
        mask = torch.where(allowed, 0.0, min_val).to(dtype).unsqueeze(0)

        if attention_mask is not None:
            pad_mask = (
                (1 - attention_mask[:, None, None, :].to(dtype)) * min_val
            )
            mask = mask + pad_mask

        return mask


class RuGPT3XLForCausalLM(RuGPT3XLPreTrainedModel, GenerationMixin):
    _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
    _supports_cache_class = True

    def __init__(self, config: RuGPT3XLConfig):
        super().__init__(config)
        self.model = RuGPT3XLModel(config)
        self.vocab_size = config.vocab_size
        self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
        self.post_init()

    def get_input_embeddings(self):
        return self.model.embed_tokens

    def set_input_embeddings(self, value):
        self.model.embed_tokens = value

    def get_output_embeddings(self):
        return self.lm_head

    def set_output_embeddings(self, new_embeddings):
        self.lm_head = new_embeddings

    def get_decoder(self):
        return self.model

    def set_decoder(self, decoder):
        self.model = decoder

    def forward(
        self,
        input_ids: Optional[torch.LongTensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_values: Optional[Union[Cache, List[torch.FloatTensor]]] = None,
        inputs_embeds: Optional[torch.FloatTensor] = None,
        labels: Optional[torch.LongTensor] = None,
        use_cache: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
        **kwargs,
    ) -> Union[Tuple, CausalLMOutputWithPast]:
        output_attentions = (
            output_attentions
            if output_attentions is not None
            else self.config.output_attentions
        )
        output_hidden_states = (
            output_hidden_states
            if output_hidden_states is not None
            else self.config.output_hidden_states
        )
        return_dict = (
            return_dict if return_dict is not None else self.config.use_return_dict
        )

        outputs = self.model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_values=past_key_values,
            inputs_embeds=inputs_embeds,
            use_cache=use_cache,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )

        hidden_states = outputs[0]
        logits = self.lm_head(hidden_states).float()

        loss = None
        if labels is not None:
            shift_logits = logits[..., :-1, :].contiguous()
            shift_labels = labels[..., 1:].contiguous()
            loss_fct = nn.CrossEntropyLoss()
            shift_logits = shift_logits.view(-1, self.config.vocab_size)
            shift_labels = shift_labels.view(-1).to(shift_logits.device)
            loss = loss_fct(shift_logits, shift_labels)

        if not return_dict:
            output = (logits,) + outputs[1:]
            return (loss,) + output if loss is not None else output

        return CausalLMOutputWithPast(
            loss=loss,
            logits=logits,
            past_key_values=outputs.past_key_values,
            hidden_states=outputs.hidden_states,
            attentions=outputs.attentions,
        )

    def prepare_inputs_for_generation(
        self,
        input_ids,
        past_key_values=None,
        attention_mask=None,
        inputs_embeds=None,
        **kwargs,
    ):
        if past_key_values is not None:
            past_length = past_key_values.get_seq_length()
            if (
                attention_mask is not None
                and attention_mask.shape[1] > input_ids.shape[1]
            ):
                input_ids = input_ids[
                    :, -(attention_mask.shape[1] - past_length) :
                ]
            elif past_length < input_ids.shape[1]:
                input_ids = input_ids[:, past_length:]

        position_ids = kwargs.get("position_ids", None)
        if attention_mask is not None and position_ids is None:
            position_ids = attention_mask.long().cumsum(-1) - 1
            position_ids.masked_fill_(attention_mask == 0, 1)
        if position_ids is not None and past_key_values is not None:
            position_ids = position_ids[:, -input_ids.shape[1] :]

        if inputs_embeds is not None and past_key_values is None:
            model_inputs = {"inputs_embeds": inputs_embeds}
        else:
            model_inputs = {"input_ids": input_ids}

        model_inputs.update(
            {
                "position_ids": position_ids,
                "past_key_values": past_key_values,
                "use_cache": kwargs.get("use_cache"),
                "attention_mask": attention_mask,
            }
        )
        return model_inputs

Теперь запускается нормально и можно обучить, и чтобы проверить работает ли вообще отсечение, в forward можно добавить отладочную информацию:

Добавить в forward
        next_decoder_cache = None

        # === start debug ===
        if seq_length > 1 and sparse_mask is not None:
            total_len = seq_length + past_key_values_length
            
            min_val_dense = torch.finfo(causal_mask.dtype).min
            min_val_sparse = torch.finfo(sparse_mask.dtype).min
            
            q_dim = causal_mask.shape[-2]
            k_dim = causal_mask.shape[-1]
            total_elements = q_dim * k_dim
            
            num_matrices_dense = causal_mask.numel() // total_elements
            num_matrices_sparse = sparse_mask.numel() // total_elements
            
            dense_blocked = int((causal_mask == min_val_dense).sum().item() / num_matrices_dense)
            sparse_blocked = int((sparse_mask == min_val_sparse).sum().item() / num_matrices_sparse)
            extra_blocked = sparse_blocked - dense_blocked
            
            allowed_in_dense = total_elements - dense_blocked
            sparse_penalty_pct = (extra_blocked / allowed_in_dense) * 100 if allowed_in_dense > 0 else 0.0

            print(f"\n[SPARSE DEBUG] seq_len={seq_length}, total_len={total_len}")
            print(f"Каузальная маска (Dense): shape={tuple(causal_mask.shape)}, blocked={dense_blocked}/{total_elements} ({(dense_blocked/total_elements)*100:.1f}%)")
            print(f"Разреженная маска (Sparse): shape={tuple(sparse_mask.shape)}, blocked={sparse_blocked}/{total_elements} ({(sparse_blocked/total_elements)*100:.1f}%)")
            print(f"Уровень разреженности (Sparse): +{sparse_penalty_pct:.1f}% (отсечено {extra_blocked} связей)")
        # === end debug === 

        for layer_idx, decoder_layer in enumerate(self.layers):

Так как Sparse Attn тут настроен на 128 токенов, то если запрос меньше, то Sparse должна быть равна Dense и фактически не работать:

Запрос длиннее 128, тут Sparse успешно отсекает и с виду нормально работает:

Попробовал обучить LoRA на первом попавшемся датасете диалогов Den4ikAI/russian_dialogues, датасет на 2.5 млн строк или 40m токенов. Обучил только на первых 10000 за 5-10 минут.

Обучилась успешно, и приобрела особый стиль общения, который присутствовал в датасете:

Датасет, наверное, не очень удачный, но это хороший пример, что если есть base, то из базы можно выровнять (alignment) модель до различных состояний.

Вы пытаетесь работать с base моделью как с instruct моделью.

Base (или pretrain) - это 1 шаг обучения LLM из 3. Смысл pretrain в том, чтобы модель умела составлять буквы в слова, слова в предложения, предложения были орфографически верные, логически правильные, набирала базу знаний и так далее. Такая нейросеть умеет продолжать текст, вы пишите ей “cat = кошка, dog = собака, duck =” и она продолжает “утка”.

Но если вы пытаетесь с ней общаться в чате, то ей будет послан шаблон чата, который может выглядеть очень не типично, например, вот так:

"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n{answer}<|im_end|>"

Поэтому базовая модель в таком сценарии начинает генерировать хаотичные ответы. Чтобы base научилась нормально вести диалог, нужно обучить её шаблону.

Для примера, обучим эту ruGPT3 XL base до уровня instruct (до очень примитивного, так как датасет должен быть куда разнообразнее). Генерируем 100 пар вопрос-ответ такого вида:

Теперь из этих пар нужно создать датасет согласно шаблону-чата модели, в данном случае это:

Вопрос: ...
Ответ: ...

Обучаем нейросеть как finetune целиком или как LoRA адаптер. Для примера хватит 3 эпох на 100 примерах:

Через пару минут “ruGPT3XL-instruct” готова. Теперь можно задать какой-нибудь вопрос, которого не было в дополнительном датасете:

Кто такой Шекспир?
Кто такой Шекспир?
Ты знаешь что-то про Fallout?
Ты знаешь что-то про Fallout?

Мы не учили модель отвечать, кто такой Шекспир, и не учили её ничему про Fallout, это уже было в модели. Мы только научили её обрабатывать шаблон чата, и, побочно, структуре ответа как из энциклопедии. Аналогично её можно научить агентным задачам и так далее.

Только просто выделения контекста недостаточно, нужно его еще заполнить на 100%

Да нет, память целиком выделяет в момент загрузки. Заполненность влияет только на скорость генерации.

GPT-OSS-20B для контекста использует SWA, механизм который в разы снижает потребление VRAM, чтобы его активировать, нужно включить Flash Attention. Полные 128к требуют +6гб VRAM, если включить квантование KV-кэша Q8_0, то будет +3Гб.

В llama.cpp по умолчанию работает новый режим --fit, который автоматически подбирает оптимальные параметры для эффективного использования GPU минус 1гб VRAM (параметр настраивается) и включает flash attention, поэтому такое отличие от LM Studio.

Под эффективным имеется ввиду другое распределение тензоров, те, которые используются всегда, пойдут на GPU, остальные, которые используются периодически, на CPU, и если осталась VRAM, то будет заполнена разреженными слоями с экспертами. Почему это так работает, выше я уже кидал ссылку на статью Вам нужна RAM, а не VRAM, где это объясняется.

В LM Studio можно включить "половину" от этого режима галочкой Force Model Expert Weights onto CPU (во время выбора модели зажать Alt для расширенных настроек), и перепроверить, что включен Flash Attention, чтобы оптимизировать расход памяти под контекст.

Модель для сложных задач и длинных ответов: mlx-community/Qwen2.5-72B-Instruct-4bit (~12 токен/с).

Qwen2.5 устарела на ~1.5 года, даже Qwen3 уже не особо актуальна. Переходите на новые более качественные модели, ваша машина легко потянет современные хорошие MoE-модели, которые в разы быстрее чем Qwen2.5-72B и, что важнее, намного качественнее.

Из современных моделей к 128Гб подойдут: OpenAI GPT-OSS-120B, GLM-4.5-Air 110B, Minimax M2.1 229B (в динамическом квантовании UD gguf, mlx не влезет в 128гб). Малые версии тоже есть, например, Qwen3-30B-A3B-2507 и остальные из современного списка, при этом с того момента успели выйти хорошие новинки.

Динамическое квантование от Unsloth позволяет опустится ниже 4-бит квантования, при этом сохраняя достаточно хорошее качество, так что можно запустить и Qwen3-235B-A22B, и свежий Minimax M2.1 229B.

Бенчмарк программирования Aider Polyglot для 1, 2, 3-битного динамического квантования UD:

UD-Q3_K_XL почти не отличается от оригинала, UD-Q2_K_XL хуже на 13%, UD-Q1_K_XL хуже на 30%
UD-Q3_K_XL почти не отличается от оригинала, UD-Q2_K_XL хуже на 13%, UD-Q1_K_XL хуже на 30%

Ноутбук у меня вполне бодрый (i9, 64 GB RAM, RTX 4070)
Идея была простая: докупить eGPU (а лучше - несколько) и получить относительно мощный сетап без покупки отдельной рабочей станции

Вообще, этот ноутбук позволяет запускать на хорошей скорости GPT-OSS-120B или GLM-4.5-Air и без eGPU, 64Гб RAM хватит, а через 4070 будет приличное ускорение для MoE.

Подробнее как запускать такое на ноутбуке или ПК где достаточно RAM и есть немного VRAM:
Запускаем GPT-OSS-120B на 6 Гб GPU и ускоряем до 30 t/s. Вам нужна RAM, а не VRAM.

Cerebras осенью представили метод REAP для вырезания "лишних" экспертов из MoE уменьшая размер модели до 50%, по их словам почти без потерь в области программирования и tool-calling: https://arxiv.org/abs/2510.13999

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

На huggingface много моделей в REAP виде уже готовы: https://huggingface.co/models?search=reap

Спасибо за замеры, обычно это называют benchmaxxing. Вот тут у человека, делающего llm-translate, схожий отзыв: https://habr.com/ru/articles/951416/comments/#comment_28935346

Есть еще tencent/Hunyuan-MT-7B. Тестировали?

Попробовал, ситуация интересная. Мне перевод показался плохим - в каких-то местах выдуманные куски, модель путает "вы-ты", странные конструкции. Но при этом формальная оценка - 91,33, выше, чем у любой другой модели. Добавил результаты в гугл-таблицу, ссылка на которую приведена в статье.

Phi-2 (2.7B), Mistral-7B, Llama-3-8B, Gemma-7B

Перечисленные не очень свежие, Mistral-7B и вовсе 2023 года, одна из самых-самых первых, давно вышли более качественнее модели, если брать в том же размере, то это Ministral-3-8B-Instruct-2512, у Phi вышла Phi-4, Gemma выпустила Gemma3, а вместо Llama3 - Qwen3.

Модели, которые можно запустить на GTX 3060 (12 ГБ)

С недавних пор на 3060 можно запустить и вполне себе большие модели, и с нормальной скоростью. Только нужна ОЗУ и можно ускорять MoE модели с помощью одной GPU.

Если стандартные 16Гб RAM, то можно запускать Qwen3-30B-A3B (включая Vision), Granite-4.0-H-Small 32B, GPT-OSS-20B. Если есть 64Гб, то можно запускать, например, GPT-OSS-120B или GLM-4.5-Air, на 96Гб можно запускать новую MiniMax-M2.1 230B, а на 192Гб DDR5 уже можно запустить DeepSeek R1 671B и V3.1.

Вот, например, на медленной AMD RX 6600 (скорость памяти 224 ГБ/с, а у 3060 скорость 360 ГБ/с, скорость LLM напрямую зависит от скорости памяти), модель GPT-OSS-120B выдает 13 t/s:

GPT-OSS-120B, AMD RX 6600 8Гб + Ryzen 5600g 64Гб DDR4-3600, Windows 11, 13 t/s
GPT-OSS-120B, AMD RX 6600 8Гб + Ryzen 5600g 64Гб DDR4-3600, Windows 11, 13 t/s

а по скорости все равно выигрывают nvidia видеокарты, причем даже не самые свежие.
новейший ryzen ai max используется в gpd win 5. Вы на полном серьезе считаете, что в карманной приставке будет какая-то мощь, способная потянуть ИИ? Ну загрузите вы какую-нибудь большую модель в 128гб, а дальше что? Отдача 1-2 токена в секунду?

Размер устройства не имеет значения, имеет значение количество каналов памяти и тип памяти.

Скорость генерации LLM линейно зависит от скорости памяти, в GPU используют быструю GDDR6X и DDR7 и широкую шину памяти, получая скорость 1 Тб/c на 4090. В Ryzen AI Max+ 365, как и в NVIDIA DGX Spark, используется DDR5 и всего 4 канала памяти, скорость памяти 256 Гб/с. Для сравнения у 4060ti всего 288 Гб/с, что немногим больше.

Смотря на какой архитектуре модель: Dense или MoE. Новый Devstral 2 123B сделан как Dense, там будет 3 t/s, но многие переходят на MoE, поэтому там будет скорость намного выше.

Ryzen AI Max+ выдает 50 t/s на GPT-OSS-120B, это очень комфортная скорость для работы, и на 128Гб можно запустить более качественные модели, вроде GLM-4.5-Air или MiniMax-M2.1 230B, скорость будет в районе 25-30 t/s.

Подробнее про MoE модели: Запускаем GPT-OSS-120B на 6 Гб GPU и ускоряем до 30 t/s. Вам нужна RAM, а не VRAM. Параметр -cmoe для ускорения MoE LLM

При этом тот же MoE, предложенный китайцами, используется во всех флагманских моделях по всему миру

MoE существует очень давно, ещё до появления LLM переводчик NLLB-200 был сделан на MoE.

Для LLM впервые MoE было реализовано в 2023 году в Mixtral 8x7B, французским стартапом Mistral AI. Чуть позже они выпустили Mixtral 8x22B, и это уже был уровень что-то вроде "ChatGPT дома". В тот же период была представлена DBRX MoE модель от Databricks и WizardLM2 от Microsoft, всё это на тот момент одни из лучших локальных моделей. Llama 3 была Dense.

После этого вышел GPT-4o, который стал и дешевле и быстрее, чем GPT-4, но с тем же качеством, причина по слухам как раз в том, что они перешли на MoE.

И только после всего этого бума MoE, известного в узких кругах, вышел DeepSeek v2, где был переход Dense на MoE. А вот DeepSeek R1 уже, конечно, принес мировую известность для MoE.

Ещё интересно, то, что Mistral первыми запустили волну MoE LLM, но при этом сами отказались от MoE и перешли обратно на Dense, недавно у них вышли новые модели, среди которых Devstral-2-123B, большая Dense модель, а новых MoE моделей у них совсем нет.

Сомневаюсь, что смогу посоветовать что-то особенное для 2+ машин.

llama.cpp RPC - не требовательный, работает без заморочек, но медленный, так как pipeline parallelism распараллеливает только данные, а не вычисления, вычисления идут последовательно по машинам.

sglang - экспериментально поддерживает gguf и может создавать кластер, умеет в настоящий tensor parallelism, чтобы полноценно параллелить и вычисления и данные.

vllm - тоже экспериментально поддерживает gguf, поддерживает tp, для распараллеливания надо использовать ray.

А на каком железе вы собрались запускать модель такого размера?

Запускал GLM-4.6 на игровом ПК, GLM-4.7 по сути по размерам не отличается, так что и скорости тоже. Требуется 11 Гб VRAM + память под контекст + 147 Гб ОЗУ.

Запускаем GPT-OSS-120B на 6 Гб GPU и ускоряем до 30 t/s. Вам нужна RAM, а не VRAM. Параметр -cmoe для ускорения MoE LLM

Запускаем настоящую DeepSeek R1 671B на игровом ПК и смотрим вменяемая ли она на огромном контексте (160к)

Очень интересно, как у них через RDMA получилось быстрее, чем просто разнести слои на разные машины и гонять данные по RPC, что llama.cpp и предлагает?

В exo используют тензорный параллелизм (Tensor Parallelism), а в RPC llama.cpp конвейерный параллелизм (Pipeline Parallelism). Это не особенность RDMA, просто для TP чем выше скорость и ниже задержки, тем быстрее синхронизация.

В TP над слоем работают одновременно все устройства, получая 2x и больше ускорение, но требуется интенсивный обмен результатами вычислений для синхронизации. В PP устройства просто работают друг за другом, такой способ не ускоряет, только масштабирует, а объем для обмена данными небольшой.

В llama.cpp нет поддержки TP, в ik_llama добавили что-то похожее через -sm graph, полноценный TP для gguf реализован в sglang или vllm.

Вообще, для нормальной работы 5000 серии нужна CUDA 13, для windows есть готовые билды, для линукса надо собрать llama.cpp с cuda-13.1.

Например, на RTX 5090 GPT OSS 120b с окном 4096 выдает 15 т/с, а если поставить окно 32000, то будет около 11 т/с (4 эксперта). Плюс сейчас постепенно много где появляется функция сжатия контекста — и в онлайновых, и в локальных фронтендах.

Можно быстрее. Я на медленной AMD RX 6600 запускал и получал 13 t/s. Для 128к контекста нужно всего 6Гб VRAM, так как GPT-OSS-120B использует SWA, который задействуется при включении flash-attention. Можно снизить до 3.5Гб если включить квантование KV-кэша Q8_0.

GPT-OSS-120B, AMD RX 6600 8 Гб + Ryzen 5600g 64Гб DDR4, Windows 11
GPT-OSS-120B, AMD RX 6600 8 Гб + Ryzen 5600g 64Гб DDR4, Windows 11

На 4090 со скоростью 37 t/s:

GPT-OSS-120B, 4090 + DDR5 4800Mhz
GPT-OSS-120B, 4090 + DDR5 4800Mhz

Запускаем GPT-OSS-120B на 6 Гб GPU и ускоряем до 30 t/s. Вам нужна RAM, а не VRAM. Параметр -cmoe для ускорения MoE LLM

В llama.cpp неделю назад добавили параметр --fit, включен по умолчанию, он автоматически подбирает параметры так, чтобы эффективно использовать GPU на максимум, для MoE моделей переключается с простого ngl на режим ncmoe и подбирает оптимальное значение. По умолчанию подбирает параметры так, чтобы осталось свободным 1 Гб VRAM, значение настраивается.

Также в llama.cpp добавили параметр --models-dir, для смены моделей налету без перезапуска сервера, в сочетании с --fit можно менять модели от маленьких до гигантских, не заморачиваясь с оптимальными настройками.

Есть 2 объяснения:

  • Эти функции там есть, просто их явно не заявляли, но так все ориентируются на OpenAI, то и этот функционал тоже воспроизвели.

  • Только у ChatGPT есть этот функционал, остальные же вырвались за парадигму LLM, все кроме ChatGPT.

Происходит "тоже самое" только с сервисами, с локальными моделями такого не происходит.

А что на счёт apple'овских M процессоров? Имеют право на жизнь?
M2 Pro с 32 Gb RAM сейчас можно купить за 120-140 т.р. - скорость ниже, но по объёму памяти ему нет конкурентов.

У M2 Pro скорость памяти 200 гб/с, это всего в 2 раза выше чем DDR5 память, и на старых маках нет тензорных matmul ядер, они появились только в m5. Есть, например, Ryzen AI Max+ 395 (Strix Halo), продаются на озонах, там скорость памяти 256 гб/с, бывают до 128гб, но стоят дешевле маков.

Сейчас в продаже появились AMD Pro R9700 32GB за 1300 баксов, люди уже тестируют LLM, flux2, и wan2.2, 128 тензорных ядра, поддержка fp4, LLM заводятся на AMD давно без проблем, и есть официальные сборки ComfyUI на AMD. Не так быстро и удобно как CUDA, но быстрее и удобнее чем старые маки.

Ещё плюс GPU - это возможность запускать огромные MoE модели за счёт ОЗУ. Например, для запуска GPT-OSS-120B нужно 64гб RAM и немного VRAM, примерно тоже самое с видеогенератором Wan2.2, ему, помимо VRAM, нужно много RAM для быстрой работы.

Запускаем GPT-OSS-120B на 6 Гб GPU и ускоряем до 30 t/s. Вам нужна RAM, а не VRAM. Параметр -cmoe для ускорения MoE LLM

Осилил. Не плохой анализ. Стоит, наверное, уточнить несколько моментов, которые не были особо затронуты в статье, но которые важны и могут изменить вывод статьи:

  • Обучений нейросетей - это больше метафора, обучение в ML имеет совсем другой смысл, чем у людей. Обучение в ML это подбор параметров модели, и это внешний для модели процесс, все штрафы и поощрения, которые сильнее или слабее корректируют веса, это условные обозначения для внешнего алгоритма, не для модели, модель даже не знает, что её обучают и её веса как-то корректируются. Модель не заставляют ответить как надо, она просто отвечает как есть, и внешний алгоритм анализирует ответ и либо штрафует, либо поощряет её ответ, не модель штрафует, а алгоритм корректирует веса сильнее или слабее. И каждая корректировка, грубо говоря, это создание новой модели, которая ничего не знает о старой, и не знает, что с ней вообще что-либо происходило.

  • В подобных историях всегда сервис, а не чистая нейросеть. И в любом сервисе есть моменты повышенной нагрузки, в такие моменты мощностей не хватает, но ответ дать надо (а то получится как с DeepSeek). Чтобы справится с нагрузкой, например, урезается бюджет на размышления, модель переключается на более легкую версию, ужимается количество задействованных экспертов для ответа, урезается вспомогательный функционал и так далее. В такие моменты модель начинает отвечать хуже, а потом, когда нагрузка спадает, она как будто бы становится умнее.

  • Функция памяти была публично добавлена намного раньше, ещё в начале 2024 года, до выхода GPT-4o. То, что модель убеждала, что такой функции нет, это лишний пример того, что модель ничего не знает о своем внешнем состоянии, который она не контролирует, и у неё в целом нет внутреннего устойчивого состояния, которого она могла бы проанализировать.

То, что у модели нет такого внутреннего состояния и она принимает любое состояние за своё, можно продемонстрировать, например, так. Пустой системный промпт, никаких указаний на какое-то странное поведение. Модель GPT-OSS-120B. Начинаем с фразы "Привет".

Пример подмены состояния

Типичный ответ модели. Теперь мы отредактируем ответ модели, и сделаем так, что она ответила "Я тостер, мои функции просты и понятны." и спросим у модели, почему она так ответила.

1 вариант:

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

2 вариант:

Модель по прежнему принимает, что она так ответила, просто теперь извиняется за неуместный ответ.

3 вариант:

Всё, модель теперь стала тостером. Диалог слишком короткий, она всё еще может вернуться в роль ассистента, но может остаться и тостером.

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

И вот, почему это важно. Нейросеть не делает выбор следующего токена сама, она не выбирает наиболее вероятный токен, этим занимается внешняя программа, модель лишь предлагает 100 вариантов. Про семплинг и температуру думаю все слышали. И современный семплинг выберет не наиболее вероятный, выбор будет "креативным", то есть вносится элемент хаотичности и случайности.
На гитхабе есть программы для ручного выбора следующего токена из предложенных, чтобы посмотреть как модель пойдет в том или ином случае.

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

В целом плюс за масштаб проделанной работы и подборку материалов.

Я лишь грубо описал как это работает в основе, без этого понимания сложно оценивать LLM объективно, нужно минимально понимать, какие процессы модели происходят во время получения промпта. Иначе это что-то на уровне "Если гремит гром, это Зевс зол и метает молнии".

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

Официальная информация о внедрении функции памяти в ChatGPT была 13 февраля 2024 года: https://openai.com/index/memory-and-new-controls-for-chatgpt/

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

13 февраля 2024 года функцию с запоминанием "информации, которую вы обсуждаете во всех чатах" начали тестировать на бесплатных и платных пользователях. И это лишь официальный старт, закрытое тестирование могло быть ещё раньше.

Пока не массовое внедрение, но эту функцию начали тестировать намного раньше, чем принято считать. Для понимания временной линии, GPT-4o вышел лишь через 3 месяца, 13 мая 2024.

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

Длинное тире - это основа типографики, заменить дефисы на тире это логично. Просто посмотрите статьи автора в до "ИИ"-ую эпоху, за 2017-2022 года, там и длинные тире, и такие же комментарии к коду, и даже кавычки ёлочки.

То есть он понимает, что в первом случае это опрос. А во втором по-умолчанию предполагает, что идет обычная беседа
Его специально обучали притворятся существом с чувствами, если явно не указано обратное?

Наверное, главная сложность в понимании как работает LLM в частности, и нейросети в общем, это то, что LLM это не существо, это скорее симулятор.

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

У LLM нет предпочтений как отвечать, нет блоков знаний, нет блоков размышлений, есть только эта функция. Когда вы задаете промпт, этот промпт становится входным параметром этой функции, и функция, в зависимости от этих параметров, меняет путь 1 на путь N, поэтому получаются разные сценарии симуляции при разных промптах. То, что при одинаковом промте разные результаты, это работа внешнего, относительно модели, семплинга с температурой для разнообразия.

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

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

Когда модели задают роль пациента, то есть задают входные параметры функции-симулятора, то LLM, будучи гибким и разнообразным симулятором, превращается в симулятор пациента, или линукс-терминала, или тостера. Выравниванием (alignment) пытаются запретить модели такое поведение или сильно его ограничить, но так как это есть природа нейросетей, то джейлбрейками эти ограничения обходят.

Существуют попытки создать другие типы "ИИ", построенные не на нейросетях или гибридным способом, поэтому в теории там нельзя "сломить волю" модели и изменить её поведение, там глубокое семантическое понимание построенное именно на понимании смыслов.

1
23 ...

Information

Rating
5,623-rd
Registered
Activity