Qwen3-4B-ensemble / ensemble_qwen.py
yushihu's picture
Update ensemble_qwen.py
ea5c483 verified
Raw
History Blame Contribute Delete
4.08 kB
# ensemble_qwen.py
import torch, math
from torch import nn
from transformers import Qwen3ForCausalLM, Qwen3Config, PreTrainedModel, PretrainedConfig
from transformers.modeling_outputs import CausalLMOutputWithPast
from transformers.generation import GenerationMixin
class EnsembleConfig(Qwen3Config):
model_type = "ensemble_qwen"
def __init__(
self,
model_a_path="Qwen/Qwen3-4B",
model_b_path="Qwen/Qwen3-4B-Base",
weight_a=0.9,
weight_b=0.1,
**kwargs,
):
super().__init__(**kwargs)
self.model_a_path, self.model_b_path = model_a_path, model_b_path
self.weight_a, self.weight_b = weight_a, weight_b
class EnsembleForCausalLM(PreTrainedModel, GenerationMixin):
config_class = EnsembleConfig
_supports_flash_attn_2 = True
_supports_sdpa = True
main_input_name = "input_ids"
_tp_plan = {"model_a.lm_head": "colwise_rep", "model_b.lm_head": "colwise_rep"}
def __init__(self, config: EnsembleConfig):
super().__init__(config)
self.weight_a = config.weight_a
self.weight_b = config.weight_b
# remove extra keys from config and initialzie Qwen3 model
qwen3_config_dict = config.to_dict()
extra_keys = ["model_a_path", "model_b_path", "weight_a", "weight_b", "auto_map"]
for key in extra_keys:
del qwen3_config_dict[key]
qwen3_config_dict["model_type"] = "qwen3"
qwen3_config_dict["architectures"] = ["Qwen3ForCausalLM"]
if hasattr(config, "attn_implementation"):
qwen3_config_dict["attn_implementation"] = config.attn_implementation
qwen3_config = Qwen3Config(**qwen3_config_dict)
self.model_a = Qwen3ForCausalLM(qwen3_config)
self.model_b = Qwen3ForCausalLM(qwen3_config)
# ---- core magic ----------------------------------------------------
def forward(
self,
input_ids: torch.LongTensor,
attention_mask: torch.LongTensor | None = None,
past_key_values: tuple | None = None,
use_cache: bool = True,
**kwargs,
) -> CausalLMOutputWithPast:
"""Run both models, sum logits, carry both kv-caches forward."""
past_a, past_b = (None, None) if past_key_values is None else past_key_values
out_a = self.model_a(
input_ids,
attention_mask=attention_mask,
past_key_values=past_a,
use_cache=use_cache,
)
out_b = self.model_b(
input_ids,
attention_mask=attention_mask,
past_key_values=past_b,
use_cache=use_cache,
)
# blend logits
logits = self.weight_a * out_a.logits + self.weight_b * out_b.logits
# pack both pasts together so generate() can recycle them next step
past = (out_a.past_key_values, out_b.past_key_values) if use_cache else None
return CausalLMOutputWithPast(
logits=logits,
past_key_values=past,
hidden_states=None,
attentions=None,
)
# ---- wiring for HF generation loop ---------------------------------
def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
# HF expects past_key_values shape to match forward() signature
if past_key_values is not None:
past_a, past_b = past_key_values
input_ids = input_ids[:, -1:] # only last token in incremental mode
else:
past_a = past_b = None
return {
"input_ids": input_ids,
"past_key_values": (past_a, past_b),
"use_cache": kwargs.get("use_cache", True),
}
def _reorder_cache(self, past, beam_idx):
# support beam search ↔ reorder both caches identically
past_a, past_b = past
past_a = self.model_a._reorder_cache(past_a, beam_idx)
past_b = self.model_b._reorder_cache(past_b, beam_idx)
return (past_a, past_b)