| from dataclasses import dataclass, field |
| from typing import Optional, Tuple, List, Dict |
| import json |
| import os |
| import shutil |
|
|
| import torch |
| import torch.nn as nn |
| from transformers import AutoConfig, AutoModelForCausalLM, PretrainedConfig, PreTrainedModel, AutoTokenizer |
| from transformers.modeling_outputs import CausalLMOutputWithPast |
|
|
| try: |
| from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training |
| PEFT_AVAILABLE = True |
| except ImportError: |
| PEFT_AVAILABLE = False |
|
|
|
|
| @dataclass |
| class SiennaConfig: |
| """Configuration for Sienna language model. |
| |
| This is a lightweight wrapper config for training. For HuggingFace compatibility, |
| use SiennaHFConfig which extends PretrainedConfig. |
| """ |
| hf_model_name: str = "Qwen/Qwen2.5-0.5B-Instruct" |
| |
| use_lora: bool = False |
| lora_r: int = 16 |
| lora_alpha: int = 32 |
| lora_dropout: float = 0.05 |
| lora_target_modules: List[str] = field(default_factory=lambda: ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]) |
| |
| gradient_checkpointing: bool = False |
| use_flash_attention: bool = True |
|
|
|
|
| def _qwen_to_sienna_key_mapping(qwen_key: str) -> str: |
| """Map Qwen weight keys to Sienna naming convention.""" |
| |
| key = qwen_key.replace("qwen2", "sienna") |
| key = key.replace("Qwen2", "Sienna") |
| |
| |
| key = key.replace("model.layers", "sienna_backbone.transformer_blocks") |
| key = key.replace("model.embed_tokens", "sienna_backbone.token_embeddings") |
| key = key.replace("model.norm", "sienna_backbone.output_norm") |
| key = key.replace("lm_head", "sienna_lm_head") |
| |
| |
| key = key.replace("self_attn", "sienna_attention") |
| key = key.replace("q_proj", "query_proj") |
| key = key.replace("k_proj", "key_proj") |
| key = key.replace("v_proj", "value_proj") |
| key = key.replace("o_proj", "output_proj") |
| |
| |
| key = key.replace("mlp.gate_proj", "feed_forward.gate_projection") |
| key = key.replace("mlp.up_proj", "feed_forward.up_projection") |
| key = key.replace("mlp.down_proj", "feed_forward.down_projection") |
| |
| |
| key = key.replace("input_layernorm", "attention_norm") |
| key = key.replace("post_attention_layernorm", "ffn_norm") |
| |
| return key |
|
|
|
|
| def _sienna_to_qwen_key_mapping(sienna_key: str) -> str: |
| """Reverse mapping: Sienna keys back to Qwen format.""" |
| key = sienna_key |
| |
| key = key.replace("sienna_backbone.transformer_blocks", "model.layers") |
| key = key.replace("sienna_backbone.token_embeddings", "model.embed_tokens") |
| key = key.replace("sienna_backbone.output_norm", "model.norm") |
| key = key.replace("sienna_lm_head", "lm_head") |
| |
| key = key.replace("sienna_attention", "self_attn") |
| key = key.replace("query_proj", "q_proj") |
| key = key.replace("key_proj", "k_proj") |
| key = key.replace("value_proj", "v_proj") |
| key = key.replace("output_proj", "o_proj") |
| |
| key = key.replace("feed_forward.gate_projection", "mlp.gate_proj") |
| key = key.replace("feed_forward.up_projection", "mlp.up_proj") |
| key = key.replace("feed_forward.down_projection", "mlp.down_proj") |
| |
| key = key.replace("attention_norm", "input_layernorm") |
| key = key.replace("ffn_norm", "post_attention_layernorm") |
| |
| return key |
|
|
|
|
| def _convert_qwen_state_dict_to_sienna(qwen_state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: |
| """Convert Qwen model weights to Sienna naming convention.""" |
| sienna_state_dict = {} |
| for key, value in qwen_state_dict.items(): |
| new_key = _qwen_to_sienna_key_mapping(key) |
| sienna_state_dict[new_key] = value |
| return sienna_state_dict |
|
|
|
|
| def save_sienna_tokenizer(hf_model_name: str, output_dir: str = "tokenizer"): |
| """Load Qwen tokenizer and save it with Sienna branding. |
| |
| Args: |
| hf_model_name: The HuggingFace model name (e.g., "Qwen/Qwen2.5-0.5B-Instruct") |
| output_dir: Directory to save the branded tokenizer (default: "tokenizer") |
| """ |
| print(f"Loading tokenizer from {hf_model_name}...") |
| tokenizer = AutoTokenizer.from_pretrained(hf_model_name) |
| |
| |
| os.makedirs(output_dir, exist_ok=True) |
| |
| |
| tokenizer.save_pretrained(output_dir) |
| print(f"✓ Tokenizer files saved to {output_dir}") |
| |
| |
| json_files = [ |
| "tokenizer_config.json", |
| "special_tokens_map.json", |
| "generation_config.json", |
| ] |
| |
| for json_file in json_files: |
| json_path = os.path.join(output_dir, json_file) |
| if os.path.exists(json_path): |
| with open(json_path, 'r', encoding='utf-8') as f: |
| config = json.load(f) |
| |
| |
| config = _replace_qwen_with_sienna_in_dict(config) |
| |
| |
| if json_file == "tokenizer_config.json": |
| config["name_or_path"] = "Sienna-Tokenizer" |
| |
| config.pop("tokenizer_class", None) |
| |
| config.pop("_sienna_base_model", None) |
| |
| with open(json_path, 'w', encoding='utf-8') as f: |
| json.dump(config, f, indent=2, ensure_ascii=False) |
| |
| print(f" ✓ Rebranded {json_file}") |
|
|
| |
| config_path = os.path.join(output_dir, "config.json") |
| config_payload = { |
| "model_type": "qwen2", |
| } |
| with open(config_path, 'w', encoding='utf-8') as f: |
| json.dump(config_payload, f, indent=2) |
| print(f" ✓ Wrote tokenizer config.json") |
| |
| print(f"\n✓ Sienna tokenizer successfully created at {output_dir}") |
| print(f" Based on: {hf_model_name}") |
| return tokenizer |
|
|
|
|
| def _replace_qwen_with_sienna_in_dict(obj): |
| """Recursively replace 'Qwen' and 'qwen' with 'Sienna' and 'sienna' in dict/list structures.""" |
| if isinstance(obj, dict): |
| return {key: _replace_qwen_with_sienna_in_dict(value) for key, value in obj.items()} |
| elif isinstance(obj, list): |
| return [_replace_qwen_with_sienna_in_dict(item) for item in obj] |
| elif isinstance(obj, str): |
| |
| result = obj.replace("Qwen", "Sienna").replace("qwen", "sienna") |
| |
| result = result.replace("QWen", "Sienna").replace("QWEN", "SIENNA") |
| return result |
| else: |
| return obj |
|
|
|
|
| class Sienna(nn.Module): |
| """Sienna Language Model - a custom branded model based on transformer architecture. |
| |
| This model uses Qwen2.5 architecture internally but presents with Sienna branding |
| and renamed layers for HuggingFace deployment. |
| """ |
| def __init__(self, config: SiennaConfig): |
| super().__init__() |
| self.config = config |
| hf_config = AutoConfig.from_pretrained(config.hf_model_name) |
| |
| |
| if config.use_flash_attention: |
| hf_config.attn_implementation = "flash_attention_2" |
| try: |
| self.model = AutoModelForCausalLM.from_config(hf_config) |
| except (ImportError, RuntimeError) as exc: |
| if config.use_flash_attention and _is_flash_attn_error(exc): |
| print(f"Flash Attention unavailable, falling back to standard attention: {exc}") |
| config.use_flash_attention = False |
| hf_config.attn_implementation = "eager" |
| self.model = AutoModelForCausalLM.from_config(hf_config) |
| else: |
| raise |
| |
| |
| if config.gradient_checkpointing: |
| self.model.gradient_checkpointing_enable() |
| |
| self._is_lora_applied = False |
| |
| |
| self.model_type = "sienna" |
| self.name_or_path = "Sienna-LM" |
|
|
| def apply_lora(self) -> None: |
| """Apply LoRA adapters for efficient fine-tuning.""" |
| if not PEFT_AVAILABLE: |
| raise ImportError("PEFT is required for LoRA. Install with: pip install peft") |
| if self._is_lora_applied: |
| return |
| |
| lora_config = LoraConfig( |
| task_type=TaskType.CAUSAL_LM, |
| r=self.config.lora_r, |
| lora_alpha=self.config.lora_alpha, |
| lora_dropout=self.config.lora_dropout, |
| target_modules=self.config.lora_target_modules, |
| bias="none", |
| ) |
| self.model = get_peft_model(self.model, lora_config) |
| self._is_lora_applied = True |
| print(f"LoRA applied. Trainable params: {self.model.print_trainable_parameters()}") |
|
|
| def forward( |
| self, |
| input_ids: torch.Tensor, |
| attention_mask: Optional[torch.Tensor] = None, |
| labels: Optional[torch.Tensor] = None, |
| past_key_values: Optional[Tuple] = None, |
| use_cache: bool = False, |
| ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple]]: |
| outputs = self.model( |
| input_ids=input_ids, |
| attention_mask=attention_mask, |
| labels=labels, |
| past_key_values=past_key_values, |
| use_cache=use_cache, |
| ) |
| return outputs.logits, outputs.loss, getattr(outputs, 'past_key_values', None) |
| |
| def get_trainable_params(self): |
| """Return only trainable parameters (useful for LoRA).""" |
| return [p for p in self.parameters() if p.requires_grad] |
| |
| def state_dict(self, *args, **kwargs) -> Dict[str, torch.Tensor]: |
| """Get state dict with Sienna-branded layer names.""" |
| |
| |
| base_state = self.model.state_dict(*args, **kwargs) |
| return _convert_qwen_state_dict_to_sienna(base_state) |
| |
| def load_state_dict(self, state_dict: Dict[str, torch.Tensor], strict: bool = True): |
| """Load state dict, handling both Sienna and Qwen key formats.""" |
| if not state_dict: |
| return self.model.load_state_dict(state_dict, strict=strict) |
|
|
| |
| sample_key = next(iter(state_dict.keys())) |
| |
| if "sienna" in sample_key.lower(): |
| |
| qwen_state = {_sienna_to_qwen_key_mapping(k): v for k, v in state_dict.items()} |
| else: |
| |
| qwen_state = state_dict |
| |
| |
| |
| return self.model.load_state_dict(qwen_state, strict=strict) |
| |
| def save_pretrained(self, save_directory: str, save_tokenizer: bool = False, **kwargs): |
| """Save model with Sienna branding for HuggingFace. |
| |
| Args: |
| save_directory: Directory to save the model |
| save_tokenizer: If True, also saves a Sienna-branded tokenizer to save_directory/tokenizer |
| """ |
| os.makedirs(save_directory, exist_ok=True) |
|
|
| |
| model_path = os.path.join(save_directory, "pytorch_model.bin") |
| torch.save(self.state_dict(), model_path) |
|
|
| |
| config_dict = { |
| "architectures": ["SiennaForCausalLM"], |
| "model_type": "sienna", |
| "hf_model_name": self.config.hf_model_name, |
| "use_lora": self.config.use_lora, |
| "lora_r": self.config.lora_r, |
| "lora_alpha": self.config.lora_alpha, |
| "lora_dropout": self.config.lora_dropout, |
| "lora_target_modules": self.config.lora_target_modules, |
| "gradient_checkpointing": self.config.gradient_checkpointing, |
| "use_flash_attention": self.config.use_flash_attention, |
| } |
| config_path = os.path.join(save_directory, "config.json") |
| with open(config_path, 'w') as f: |
| json.dump(config_dict, f, indent=2) |
|
|
| print(f"✓ Sienna model saved to {save_directory}") |
| print(f" Model architecture: Sienna (based on Qwen2.5)") |
| print(f" Layer naming: Sienna convention for HuggingFace deployment") |
|
|
| |
| if save_tokenizer: |
| tokenizer_dir = os.path.join(save_directory, "tokenizer") |
| save_sienna_tokenizer(self.config.hf_model_name, tokenizer_dir) |
| print(f" ✓ Tokenizer saved to {tokenizer_dir}") |
|
|
| print(f"\n✓ Sienna model saved successfully") |
|
|
|
|
| |
| SiennaModel = Sienna |
|
|
|
|
| @torch.no_grad() |
| def load_sienna_weights( |
| model: Sienna, |
| hf_model_name: Optional[str] = None, |
| dtype: Optional[torch.dtype] = None, |
| model_cache_dir: str = "model", |
| ) -> None: |
| """Load Sienna model weights, using cached version if available. |
| |
| This function first checks if a cached Sienna model exists in the model_cache_dir. |
| If found, it loads from cache. Otherwise, it downloads from HuggingFace, |
| transforms to Sienna naming, and saves to cache for future use. |
| |
| Args: |
| model: The Sienna model to load weights into |
| hf_model_name: Optional override for the model name (defaults to config's hf_model_name) |
| dtype: Optional dtype (torch.bfloat16 recommended for training) |
| model_cache_dir: Directory to cache the transformed Sienna model (default: "model") |
| """ |
| load_name = hf_model_name or model.config.hf_model_name |
| |
| |
| model_path = os.path.join(model_cache_dir, "pytorch_model.bin") |
| config_path = os.path.join(model_cache_dir, "config.json") |
| tokenizer_path = os.path.join(model_cache_dir, "tokenizer") |
| |
| if os.path.exists(model_path) and os.path.exists(config_path): |
| print(f"✓ Found cached Sienna model in '{model_cache_dir}/'") |
| print(f" Loading pretrained Sienna weights from cache...") |
| |
| |
| state_dict = torch.load(model_path, map_location='cpu') |
| |
| |
| if dtype: |
| state_dict = {k: v.to(dtype) if v.is_floating_point() else v for k, v in state_dict.items()} |
| |
| |
| qwen_state = {_sienna_to_qwen_key_mapping(k): v for k, v in state_dict.items()} |
| |
| |
| target_model = model.model.base_model.model if model._is_lora_applied else model.model |
| missing, unexpected = target_model.load_state_dict(qwen_state, strict=False) |
| |
| if missing: |
| print(f" Missing keys: {len(missing)}") |
| if unexpected: |
| print(f" Unexpected keys: {len(unexpected)}") |
| |
| print(f"✓ Successfully loaded cached Sienna model") |
| return |
| |
| |
| print(f"No cached model found in '{model_cache_dir}/'") |
| print(f"Downloading from HuggingFace: {load_name}") |
| print(f"This will be cached for future use...") |
| |
| load_kwargs = { |
| "low_cpu_mem_usage": True, |
| } |
| if dtype: |
| load_kwargs["dtype"] = dtype |
| |
| |
| if model.config.use_flash_attention: |
| load_kwargs["attn_implementation"] = "flash_attention_2" |
|
|
| try: |
| hf_model = AutoModelForCausalLM.from_pretrained(load_name, **load_kwargs) |
| except (ImportError, RuntimeError) as exc: |
| if model.config.use_flash_attention and _is_flash_attn_error(exc): |
| print(f"Flash Attention unavailable while loading weights, retrying with standard attention: {exc}") |
| model.config.use_flash_attention = False |
| load_kwargs.pop("attn_implementation", None) |
| hf_model = AutoModelForCausalLM.from_pretrained(load_name, **load_kwargs) |
| else: |
| raise |
| |
| |
| source_state_dict = hf_model.state_dict() |
| |
| |
| target_model = model.model.base_model.model if model._is_lora_applied else model.model |
| missing, unexpected = target_model.load_state_dict(source_state_dict, strict=False) |
| |
| if missing: |
| print(f"Missing keys: {len(missing)}") |
| if unexpected: |
| print(f"Unexpected keys: {len(unexpected)}") |
| |
| print(f"✓ Successfully loaded weights from {load_name}") |
| |
| del hf_model |
| |
| |
| print(f"\nCaching Sienna model to '{model_cache_dir}/' for future use...") |
| os.makedirs(model_cache_dir, exist_ok=True) |
| |
| |
| sienna_state_dict = model.state_dict() |
| |
| |
| if dtype: |
| sienna_state_dict = {k: v.cpu().to(torch.float32) if v.is_floating_point() else v.cpu() |
| for k, v in sienna_state_dict.items()} |
| else: |
| sienna_state_dict = {k: v.cpu() for k, v in sienna_state_dict.items()} |
| |
| |
| torch.save(sienna_state_dict, model_path) |
| print(f" ✓ Saved model weights: {model_path}") |
| |
| |
| config_dict = { |
| "architectures": ["SiennaForCausalLM"], |
| "model_type": "sienna", |
| "base_model": load_name, |
| "vocab_size": 151936, |
| "hidden_size": getattr(model.model.config, 'hidden_size', 896), |
| "num_hidden_layers": getattr(model.model.config, 'num_hidden_layers', 24), |
| "num_attention_heads": getattr(model.model.config, 'num_attention_heads', 14), |
| } |
| with open(config_path, 'w') as f: |
| json.dump(config_dict, f, indent=2) |
| print(f" ✓ Saved config: {config_path}") |
| |
| |
| save_sienna_tokenizer(load_name, tokenizer_path) |
| print(f" ✓ Saved tokenizer: {tokenizer_path}/") |
| |
| print(f"\n✓ Sienna model cached successfully!") |
| print(f" Next time you run, it will load instantly from '{model_cache_dir}/'") |
|
|
|
|
| |
| def load_qwen_weights(*args, **kwargs): |
| """Deprecated: Use load_sienna_weights instead.""" |
| print("Warning: load_qwen_weights is deprecated, use load_sienna_weights instead") |
| return load_sienna_weights(*args, **kwargs) |
|
|
|
|
| def _is_flash_attn_error(exc: Exception) -> bool: |
| text = str(exc).lower() |
| return ( |
| "flash_attn" in text |
| or "flash attention" in text |
| or "attn_implementation" in text |
| or "undefined symbol" in text |
| ) |
|
|