char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
dd4623a74
char/aliceai-t5-moe
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4from __future__import annotations 5 6from dataclasses import dataclass 7import logging 8import argparse 9import os 10import sys 11import json 12from math import prod 13from pathlib import Path 14from typing import TYPE_CHECKING ,Any ,Callable ,Iterable ,Iterator ,Sequence ,SupportsIndex ,cast 15from transformers import AutoConfig ,AutoTokenizer 16 17import torch 18 19if TYPE_CHECKING : 20from torch import Tensor 21 22if 'NO_LOCAL_GGUF' not in os .environ : 23sys .path .insert (1 ,str (Path (__file__ ).parent / 'gguf-py' )) 24import gguf 25from gguf .constants import GGUFValueType 26 27# reuse model definitions from the conversion/ package 28from conversion import LazyTorchTensor ,ModelBase ,get_model_class ,ModelType ,get_model_architecture 29 30logger = logging .getLogger ("lora-to-gguf" ) 31 32 33@dataclass 34class PartialLoraTensor : 35A :Tensor | None = None 36B :Tensor | None = None 37 38 39# magic to support tensor shape modifications and splitting 40class LoraTorchTensor : 41_lora_A :Tensor # (n_rank, row_size) 42_lora_B :Tensor # (col_size, n_rank) 43_rank :int 44 45def __init__ (self ,A :Tensor ,B :Tensor ): 46assert len (A .shape )== len (B .shape ) 47assert A .shape [- 2 ]== B .shape [- 1 ] 48if A .dtype != B .dtype : 49A = A .to (torch .float32 ) 50B = B .to (torch .float32 ) 51self ._lora_A = A 52self ._lora_B = B 53self ._rank = B .shape [- 1 ] 54 55def get_lora_A_B (self )-> tuple [Tensor ,Tensor ]: 56return (self ._lora_A ,self ._lora_B ) 57 58def __getitem__ ( 59self , 60indices : ( 61SupportsIndex 62| slice 63| tuple [SupportsIndex | slice | Tensor , ...]# TODO: add ellipsis in the type signature 64 ), 65 )-> LoraTorchTensor : 66shape = self .shape 67if isinstance (indices ,SupportsIndex ): 68if len (shape )> 2 : 69return LoraTorchTensor (self ._lora_A [indices ],self ._lora_B [indices ]) 70else : 71raise NotImplementedError # can't return a vector 72elif isinstance (indices ,slice ): 73if len (shape )> 2 : 74return LoraTorchTensor (self ._lora_A [indices ],self ._lora_B [indices ]) 75else : 76return LoraTorchTensor (self ._lora_A ,self ._lora_B [indices ]) 77elif isinstance (indices ,tuple ): 78assert len (indices )> 0 79if indices [- 1 ]is Ellipsis : 80return self [indices [:- 1 ]] 81# expand ellipsis 82indices = tuple ( 83u 84for v in ( 85 ( 86 (slice (None ,None )for _ in range (len (indices )- 1 )) 87if i is Ellipsis 88else (i ,) 89 ) 90for i in indices 91 ) 92for u in v 93 ) 94 95if len (indices )< len (shape ): 96indices = (* indices ,* (slice (None ,None )for _ in range (len (indices ),len (shape )))) 97 98# TODO: make sure this is correct 99indices_A = ( 100* ( 101 ( 102j .__index__ ()% self ._lora_A .shape [i ] 103if isinstance (j ,SupportsIndex ) 104else slice (None ,None ) 105 ) 106for i ,j in enumerate (indices [:- 2 ]) 107 ), 108slice (None ,None ), 109indices [- 1 ], 110 ) 111indices_B = indices [:- 1 ] 112return LoraTorchTensor (self ._lora_A [indices_A ],self ._lora_B [indices_B ]) 113else : 114raise NotImplementedError # unknown indice type 115 116@property 117def dtype (self )-> torch .dtype : 118assert self ._lora_A .dtype == self ._lora_B .dtype 119return self ._lora_A .dtype 120 121@property 122def shape (self )-> tuple [int , ...]: 123assert len (self ._lora_A .shape )== len (self ._lora_B .shape ) 124return (* self ._lora_B .shape [:- 1 ],self ._lora_A .shape [- 1 ]) 125 126def size (self ,dim = None ): 127assert dim is None 128return self .shape 129 130def contiguous (self )-> LoraTorchTensor : 131return LoraTorchTensor ( 132self ._lora_A .contiguous (), 133self ._lora_B .contiguous (), 134 ) 135 136def reshape (self ,* shape :int | tuple [int , ...])-> LoraTorchTensor : 137if isinstance (shape [0 ],tuple ): 138new_shape :tuple [int , ...]= shape [0 ] 139else : 140new_shape = cast (tuple [int , ...],shape ) 141orig_shape = self .shape 142if len (new_shape )< 2 : 143raise NotImplementedError # can't become a vector 144 145# expand -1 in the shape 146if any (dim == - 1 for dim in new_shape ): 147n_elems = prod (orig_shape ) 148n_new_elems = prod (dim if dim != - 1 else 1 for dim in new_shape ) 149assert n_elems % n_new_elems == 0 150new_shape = (* (dim if dim != - 1 else n_elems // n_new_elems for dim in new_shape ),) 151 152if new_shape [- 1 ]!= orig_shape [- 1 ]: 153raise NotImplementedError # can't reshape the row size trivially 154 155shape_A = (* (1 for _ in new_shape [:- 2 ]),self ._rank ,orig_shape [- 1 ]) 156shape_B = (* new_shape [:- 1 ],self ._rank ) 157return LoraTorchTensor ( 158self ._lora_A .reshape (shape_A ), 159self ._lora_B .reshape (shape_B ), 160 ) 161 162def reshape_as (self ,other :Tensor )-> LoraTorchTensor : 163return self .reshape (* other .shape ) 164 165def view (self ,* size :int )-> LoraTorchTensor : 166return self .reshape (* size ) 167 168def permute (self ,* dims :int )-> LoraTorchTensor : 169shape = self .shape 170dims = tuple (dim - len (shape )if dim >= 0 else dim for dim in dims ) 171if dims [- 1 ]== - 1 : 172# TODO: support higher dimensional A shapes bigger than 1 173assert all (dim == 1 for dim in self ._lora_A .shape [:- 2 ]) 174return LoraTorchTensor (self ._lora_A ,self ._lora_B .permute (* dims )) 175if len (shape )== 2 and dims [- 1 ]== - 2 and dims [- 2 ]== - 1 : 176return LoraTorchTensor (self ._lora_B .permute (* dims ),self ._lora_A .permute (* dims )) 177else : 178# TODO: compose the above two 179raise NotImplementedError 180 181def transpose (self ,dim0 :int ,dim1 :int )-> LoraTorchTensor : 182shape = self .shape 183dims = [i for i in range (len (shape ))] 184dims [dim0 ],dims [dim1 ]= dims [dim1 ],dims [dim0 ] 185return self .permute (* dims ) 186 187def swapaxes (self ,axis0 :int ,axis1 :int )-> LoraTorchTensor : 188return self .transpose (axis0 ,axis1 ) 189 190def split (self ,split_size :int | Sequence [int ],dim :int = 0 )-> tuple [LoraTorchTensor , ...]: 191shape = self .shape 192ndim = len (shape ) 193if dim < 0 : 194dim += ndim 195if dim == ndim - 1 : 196A_chunks = self ._lora_A .split (split_size ,dim =- 1 ) 197return tuple (LoraTorchTensor (a ,self ._lora_B )for a in A_chunks ) 198elif dim == ndim - 2 : 199B_chunks = self ._lora_B .split (split_size ,dim =- 2 ) 200return tuple (LoraTorchTensor (self ._lora_A ,b )for b in B_chunks ) 201else : 202B_chunks = self ._lora_B .split (split_size ,dim = dim ) 203if self ._lora_A .shape [dim ]== 1 : 204return tuple (LoraTorchTensor (self ._lora_A ,b )for b in B_chunks ) 205A_chunks = self ._lora_A .split (split_size ,dim = dim ) 206return tuple (LoraTorchTensor (a ,b )for a ,b in zip (A_chunks ,B_chunks )) 207 208def to (self ,* args ,** kwargs ): 209return LoraTorchTensor (self ._lora_A .to (* args ,** kwargs ),self ._lora_B .to (* args ,** kwargs )) 210 211def __mul__ (self ,other )-> LoraTorchTensor : 212# Only output-side multiplication for now 213# W = B @ A, so M_out * W == (M_out * B) @ A 214if not isinstance (other , (int ,float ))and other .shape and other .shape [- 1 ]!= 1 : 215raise NotImplementedError 216return LoraTorchTensor (self ._lora_A ,self ._lora_B * other ) 217 218def __rmul__ (self ,other )-> LoraTorchTensor : 219return self * other 220 221@classmethod 222def __torch_function__ (cls ,func :Callable ,types ,args = (),kwargs = None ): 223del types # unused 224 225if kwargs is None : 226kwargs = {} 227 228if func is torch .permute : 229assert len (args ) 230return type (args [0 ]).permute (* args ,** kwargs ) 231elif func is torch .reshape : 232assert len (args ) 233return type (args [0 ]).reshape (* args ,** kwargs ) 234elif func is torch .stack : 235assert len (args ) 236assert isinstance (args [0 ],Sequence ) 237dim = kwargs .get ("dim" ,0 ) 238assert dim == 0 239return LoraTorchTensor ( 240torch .stack ([a ._lora_A for a in args [0 ]],dim ), 241torch .stack ([b ._lora_B for b in args [0 ]],dim ), 242 ) 243elif func is torch .cat : 244assert len (args ) 245assert isinstance (args [0 ],Sequence ) 246dim = kwargs .get ("dim" ,0 ) 247assert dim == 0 248if len (args [0 ][0 ].shape )> 2 : 249return LoraTorchTensor ( 250torch .cat ([a ._lora_A for a in args [0 ]],dim ), 251torch .cat ([b ._lora_B for b in args [0 ]],dim ), 252 ) 253elif all (torch .equal (args [0 ][0 ]._lora_A ,t ._lora_A )for t in args [0 ][1 :]): 254return LoraTorchTensor ( 255args [0 ][0 ]._lora_A , 256torch .cat ([b ._lora_B for b in args [0 ]],dim ), 257 ) 258else : 259raise NotImplementedError 260elif func is torch .split : 261assert len (args )and len (args )>= 2 262tensor ,split_size = args [0 ],args [1 ] 263dim = args [2 ]if len (args )> 2 else kwargs .get ("dim" ,0 ) 264return tensor .split (split_size ,dim = dim ) 265else : 266raise NotImplementedError 267 268 269def get_base_tensor_name (lora_tensor_name :str )-> str : 270base_name = lora_tensor_name .replace ("base_model.model." ,"" ) 271base_name = base_name .replace (".lora_A.weight" ,".weight" ) 272base_name = base_name .replace (".lora_B.weight" ,".weight" ) 273# models produced by mergekit-extract-lora have token embeddings in the adapter 274base_name = base_name .replace (".lora_embedding_A" ,".weight" ) 275base_name = base_name .replace (".lora_embedding_B" ,".weight" ) 276return base_name 277 278 279def parse_args ()-> argparse .Namespace : 280parser = argparse .ArgumentParser ( 281description = "Convert a Hugging Face PEFT LoRA adapter to a GGUF file" ) 282parser .add_argument ( 283"--outfile" ,type = Path , 284help = "path to write to; default: based on input. {ftype} will be replaced by the outtype." , 285 ) 286parser .add_argument ( 287"--outtype" ,type = str ,choices = ["f32" ,"f16" ,"bf16" ,"q8_0" ,"auto" ],default = "f32" , 288help = "output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, auto for the highest-fidelity 16-bit float type depending on the first loaded tensor type" , 289 ) 290parser .add_argument ( 291"--bigendian" ,action = "store_true" , 292help = "model is executed on big endian machine" , 293 ) 294parser .add_argument ( 295"--no-lazy" ,action = "store_true" , 296help = "use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)" , 297 ) 298parser .add_argument ( 299"--verbose" ,action = "store_true" , 300help = "increase output verbosity" , 301 ) 302parser .add_argument ( 303"--dry-run" ,action = "store_true" , 304help = "only print out what will be done, without writing any new files" , 305 ) 306parser .add_argument ( 307"--base" ,type = Path , 308help = "directory containing Hugging Face model config files (config.json, tokenizer.json) for the base model that the adapter is based on - only config is needed, actual model weights are not required. If base model is unspecified, it will be loaded from Hugging Face hub based on the adapter config" , 309 ) 310parser .add_argument ( 311"--base-model-id" ,type = str , 312help = "the model ID of the base model, if it is not available locally or in the adapter config. If specified, it will ignore --base and load the base model config from the Hugging Face hub (Example: 'meta-llama/Llama-3.2-1B-Instruct')" , 313 ) 314parser .add_argument ( 315"--trust-remote-code" ,default = False ,action = "store_true" , 316help = "trust remote code in the model" , 317 ) 318parser .add_argument ( 319"lora_path" ,type = Path , 320help = "directory containing Hugging Face PEFT LoRA config (adapter_model.json) and weights (adapter_model.safetensors or adapter_model.bin)" , 321 ) 322 323return parser .parse_args () 324 325 326def load_hparams_from_hf (hf_model_id :str ,trust_remote_code :bool )-> tuple [dict [str ,Any ],Path | None ]: 327from huggingface_hub import try_to_load_from_cache 328 329# normally, adapter does not come with base model config, we need to load it from AutoConfig 330config = AutoConfig .from_pretrained (hf_model_id ,trust_remote_code = trust_remote_code ) 331cache_dir = try_to_load_from_cache (hf_model_id ,"config.json" ) 332cache_dir = Path (cache_dir ).parent if isinstance (cache_dir ,str )else None 333 334return config .to_dict (),cache_dir 335 336 337if __name__ == '__main__' : 338args = parse_args () 339logging .basicConfig (level = logging .DEBUG if args .verbose else logging .INFO ) 340 341ftype_map :dict [str ,gguf .LlamaFileType ]= { 342"f32" :gguf .LlamaFileType .ALL_F32 , 343"f16" :gguf .LlamaFileType .MOSTLY_F16 , 344"bf16" :gguf .LlamaFileType .MOSTLY_BF16 , 345"q8_0" :gguf .LlamaFileType .MOSTLY_Q8_0 , 346"auto" :gguf .LlamaFileType .GUESSED , 347 } 348 349ftype = ftype_map [args .outtype ] 350 351dir_base_model :Path | None = args .base 352dir_lora :Path = args .lora_path 353base_model_id :str | None = args .base_model_id 354lora_config = dir_lora / "adapter_config.json" 355input_model = dir_lora / "adapter_model.safetensors" 356 357if args .outfile is not None : 358fname_out = args .outfile 359else : 360# output in the same directory as the model by default 361fname_out = dir_lora 362 363if os .path .exists (input_model ): 364# lazy import load_file only if lora is in safetensors format. 365from safetensors .torch import load_file 366 367lora_model = load_file (input_model ,device = "cpu" ) 368else : 369input_model = os .path .join (dir_lora ,"adapter_model.bin" ) 370lora_model = torch .load (input_model ,map_location = "cpu" ,weights_only = True ) 371 372# load LoRA config 373with open (lora_config ,"r" )as f : 374lparams :dict [str ,Any ]= json .load (f ) 375 376# load base model 377if base_model_id is not None : 378logger .info (f"Loading base model from Hugging Face: { base_model_id } " ) 379hparams ,dir_base_model = load_hparams_from_hf (base_model_id ,args .trust_remote_code ) 380elif dir_base_model is None : 381if "base_model_name_or_path" in lparams : 382model_id = lparams ["base_model_name_or_path" ] 383logger .info (f"Loading base model from Hugging Face: { model_id } " ) 384try : 385hparams ,dir_base_model = load_hparams_from_hf (model_id ,args .trust_remote_code ) 386except OSError as e : 387logger .error (f"Failed to load base model config: { e } " ) 388logger .error ("Please try downloading the base model and add its path to --base" ) 389sys .exit (1 ) 390else : 391logger .error ("'base_model_name_or_path' is not found in adapter_config.json" ) 392logger .error ("Base model config is required. Please download the base model and add its path to --base" ) 393sys .exit (1 ) 394else : 395logger .info (f"Loading base model: { dir_base_model . name } " ) 396hparams = ModelBase .load_hparams (dir_base_model ,False ) 397 398with torch .inference_mode (): 399model_arch = get_model_architecture (hparams ,ModelType .TEXT ) 400try : 401model_class = get_model_class (model_arch ) 402logger .info ("Using model architecture: %s" ,model_arch ) 403except NotImplementedError : 404logger .error (f"Model { model_arch } is not supported" ) 405sys .exit (1 ) 406 407class LoraModel (model_class ):# ty: ignore[unsupported-base] 408model_arch = model_class .model_arch 409 410lora_alpha :float 411 412def __init__ (self ,* args ,dir_lora_model :Path ,lora_alpha :float ,** kwargs ): 413 414super ().__init__ (* args ,** kwargs ) 415 416self .dir_model_card = dir_lora_model 417self .lora_alpha = float (lora_alpha ) 418 419def set_vocab (self ): 420pass 421 422def set_type (self ): 423self .gguf_writer .add_type (gguf .GGUFType .ADAPTER ) 424self .gguf_writer .add_string (gguf .Keys .Adapter .TYPE ,"lora" ) 425 426def set_gguf_parameters (self ): 427logger .debug ("GGUF KV: %s = %d" ,gguf .Keys .Adapter .LORA_ALPHA ,self .lora_alpha ) 428self .gguf_writer .add_float32 (gguf .Keys .Adapter .LORA_ALPHA ,self .lora_alpha ) 429alora_invocation_tokens = lparams .get ("alora_invocation_tokens" ) 430invocation_string = lparams .get ("invocation_string" ) 431if invocation_string and not alora_invocation_tokens : 432logger .debug ("Tokenizing invocation_string -> alora_invocation_tokens" ) 433base_model_path_or_id = hparams .get ("_name_or_path" ) 434try : 435tokenizer = AutoTokenizer .from_pretrained (base_model_path_or_id ) 436except ValueError : 437logger .error ("Unable to load tokenizer from %s" ,base_model_path_or_id ) 438raise 439# NOTE: There's an off-by-one with the older aLoRAs where 440# the invocation string includes the "<|start_of_turn|>" 441# token, but the adapters themselves were trained to 442# activate _after_ that first token, so we drop it here. 443alora_invocation_tokens = tokenizer (invocation_string )["input_ids" ][1 :]# ty: ignore[call-non-callable] 444if alora_invocation_tokens : 445logger .debug ("GGUF KV: %s = %s" ,gguf .Keys .Adapter .ALORA_INVOCATION_TOKENS ,alora_invocation_tokens ) 446self .gguf_writer .add_key_value ( 447gguf .Keys .Adapter .ALORA_INVOCATION_TOKENS , 448alora_invocation_tokens , 449GGUFValueType .ARRAY , 450GGUFValueType .UINT32 , 451 ) 452 453def generate_extra_tensors (self )-> Iterable [tuple [str ,Tensor ]]: 454# Never add extra tensors (e.g. rope_freqs) for LoRA adapters 455return () 456 457def get_tensors (self )-> Iterator [tuple [str ,Tensor ]]: 458tensor_map :dict [str ,PartialLoraTensor ]= {} 459 460for name ,tensor in lora_model .items (): 461if self .lazy : 462tensor = LazyTorchTensor .from_eager (tensor ) 463base_name = get_base_tensor_name (name ) 464# filter base name, ignore tensor transformations for now 465data_gen = lambda g = tensor :g # noqa: E731 466if (titem := self .filter_tensors ((base_name ,data_gen )))is None : 467continue 468base_name ,_ = titem 469# note: mergekit-extract-lora also adds token embeddings to the adapter 470is_lora_a = ".lora_A.weight" in name or ".lora_embedding_A" in name 471is_lora_b = ".lora_B.weight" in name or ".lora_embedding_B" in name 472if not is_lora_a and not is_lora_b : 473if ".base_layer.weight" in name : 474continue 475# mergekit-extract-lora add these layernorm to the adapter, we need to keep them 476if "_layernorm" in name or ".norm" in name : 477yield (base_name ,tensor ) 478continue 479logger .error (f"Unexpected name ' { name } ': Not a lora_A or lora_B tensor" ) 480if ".embed_tokens.weight" in name or ".lm_head.weight" in name : 481logger .error ("Embeddings is present in the adapter. This can be due to new tokens added during fine tuning" ) 482logger .error ("Please refer to https://github.com/ggml-org/llama.cpp/pull/9948" ) 483sys .exit (1 ) 484 485if base_name in tensor_map : 486if is_lora_a : 487tensor_map [base_name ].A = tensor 488else : 489tensor_map [base_name ].B = tensor 490else : 491if is_lora_a : 492tensor_map [base_name ]= PartialLoraTensor (A = tensor ) 493else : 494tensor_map [base_name ]= PartialLoraTensor (B = tensor ) 495 496for name ,tensor in tensor_map .items (): 497assert tensor .A is not None 498assert tensor .B is not None 499yield (name ,cast (torch .Tensor ,LoraTorchTensor (tensor .A ,tensor .B ))) 500 501def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 502dest = list (super ().modify_tensors (data_torch ,name ,bid )) 503# some archs may have the same tensor for lm_head and output (tie word embeddings) 504# in this case, adapters targeting lm_head will fail when using llama-export-lora 505# therefore, we ignore them for now 506# see: https://github.com/ggml-org/llama.cpp/issues/9065 507if name == "lm_head.weight" and len (dest )== 0 : 508raise ValueError ("lm_head is present in adapter, but is ignored in base model" ) 509for dest_name ,dest_data in dest : 510# mergekit-extract-lora add these layernorm to the adapter 511if "_norm" in dest_name : 512assert dest_data .dim ()== 1 513yield (dest_name ,dest_data ) 514continue 515 516# otherwise, we must get the lora_A and lora_B tensors 517assert isinstance (dest_data ,LoraTorchTensor ) 518lora_a ,lora_b = dest_data .get_lora_A_B () 519 520# note: mergekit-extract-lora flip and transpose A and B 521# here we only need to transpose token_embd.lora_a, see llm_build_inp_embd() 522if "token_embd.weight" in dest_name : 523lora_a = lora_a .T 524 525yield (dest_name + ".lora_a" ,lora_a ) 526yield (dest_name + ".lora_b" ,lora_b ) 527 528alpha :float = lparams ["lora_alpha" ] 529 530model_instance = LoraModel ( 531dir_base_model , 532ftype , 533fname_out , 534is_big_endian = args .bigendian , 535use_temp_file = False , 536eager = args .no_lazy , 537dry_run = args .dry_run , 538dir_lora_model = dir_lora , 539lora_alpha = alpha , 540hparams = hparams , 541remote_hf_model_id = base_model_id , 542 ) 543 544logger .info ("Exporting model..." ) 545model_instance .write () 546logger .info (f"Model successfully exported to { model_instance . fname_out } " )