char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
8f4880738
char/aliceai-t5-moe
1from __future__import annotations 2 3from dataclasses import dataclass 4from pathlib import Path 5from typing import Literal 6 7import os 8import json 9import numpy as np 10 11 12def fill_templated_filename (filename :str ,output_type :str | None )-> str : 13# Given a file name fill in any type templates e.g. 'some-model-name.{ftype}.gguf' 14ftype_lowercase :str = output_type .lower ()if output_type is not None else "" 15ftype_uppercase :str = output_type .upper ()if output_type is not None else "" 16return filename .format (ftype_lowercase , 17outtype = ftype_lowercase ,ftype = ftype_lowercase , 18OUTTYPE = ftype_uppercase ,FTYPE = ftype_uppercase ) 19 20 21def model_weight_count_rounded_notation (model_params_count :int ,min_digits :int = 2 )-> str : 22if model_params_count > 1e12 : 23# Trillions Of Parameters 24scaled_model_params = model_params_count * 1e-12 25scale_suffix = "T" 26elif model_params_count > 1e9 : 27# Billions Of Parameters 28scaled_model_params = model_params_count * 1e-9 29scale_suffix = "B" 30elif model_params_count > 1e6 : 31# Millions Of Parameters 32scaled_model_params = model_params_count * 1e-6 33scale_suffix = "M" 34else : 35# Thousands Of Parameters 36scaled_model_params = model_params_count * 1e-3 37scale_suffix = "K" 38 39fix = max (min_digits - len (str (round (scaled_model_params )).lstrip ('0' )),0 ) 40 41return f" { scaled_model_params :.{ fix }f } { scale_suffix } " 42 43 44def size_label (total_params :int ,shared_params :int ,expert_params :int ,expert_count :int )-> str : 45 46if expert_count > 0 : 47pretty_size = model_weight_count_rounded_notation (abs (shared_params )+ abs (expert_params ),min_digits = 2 ) 48size_class = f" { expert_count } x { pretty_size } " 49else : 50size_class = model_weight_count_rounded_notation (abs (total_params ),min_digits = 2 ) 51 52return size_class 53 54 55def naming_convention (model_name :str | None ,base_name :str | None ,finetune_string :str | None ,version_string :str | None ,size_label :str | None ,output_type :str | None ,model_type :Literal ['vocab' ,'LoRA' ]| None = None )-> str : 56# Reference: https://github.com/ggml-org/ggml/blob/master/docs/gguf.md#gguf-naming-convention 57 58if base_name is not None : 59name = base_name .strip ().replace (' ' ,'-' ).replace ('/' ,'-' ) 60elif model_name is not None : 61name = model_name .strip ().replace (' ' ,'-' ).replace ('/' ,'-' ) 62else : 63name = "ggml-model" 64 65parameters = f"- { size_label } " if size_label is not None else "" 66 67finetune = f"- { finetune_string . strip (). replace ( ' ' , '-' ) } " if finetune_string is not None else "" 68 69version = f"- { version_string . strip (). replace ( ' ' , '-' ) } " if version_string is not None else "" 70 71encoding = f"- { output_type . strip (). replace ( ' ' , '-' ). upper () } " if output_type is not None else "" 72 73kind = f"- { model_type . strip (). replace ( ' ' , '-' ) } " if model_type is not None else "" 74 75return f" { name } { parameters } { finetune } { version } { encoding } { kind } " 76 77 78@dataclass 79class RemoteTensor : 80dtype :str 81shape :tuple [int , ...] 82offset_start :int 83size :int 84url :str 85 86def data (self )-> bytearray : 87# TODO: handle request errors (maybe with limited retries?) 88# NOTE: using a bytearray, otherwise PyTorch complains the buffer is not writeable 89data = bytearray (SafetensorRemote .get_data_by_range (url = self .url ,start = self .offset_start ,size = self .size )) 90return data 91 92 93class SafetensorRemote : 94""" 95Uility class to handle remote safetensor files. 96This class is designed to work with Hugging Face model repositories. 97 98Example (one model has single safetensor file, the other has multiple): 99for model_id in ["ngxson/TEST-Tiny-Llama4", "Qwen/Qwen2.5-7B-Instruct"]: 100tensors = SafetensorRemote.get_list_tensors_hf_model(model_id) 101print(tensors) 102 103Example reading tensor data: 104tensors = SafetensorRemote.get_list_tensors_hf_model(model_id) 105for name, meta in tensors.items(): 106dtype, shape, offset_start, size, remote_safetensor_url = meta 107# read the tensor data 108data = SafetensorRemote.get_data_by_range(remote_safetensor_url, offset_start, size) 109print(data) 110""" 111 112BASE_DOMAIN = "https://huggingface.co" 113 114@classmethod 115def get_list_tensors_hf_model (cls ,model_id :str )-> dict [str ,RemoteTensor ]: 116""" 117Get list of tensors from a Hugging Face model repository. 118 119Returns a dictionary of tensor names and their metadata. 120Each tensor is represented as a tuple of (dtype, shape, offset_start, size, remote_safetensor_url) 121""" 122# case 1: model has only one single model.safetensor file 123is_single_file = cls .check_file_exist (f" { cls . BASE_DOMAIN } / { model_id } /resolve/main/model.safetensors" ) 124if is_single_file : 125url = f" { cls . BASE_DOMAIN } / { model_id } /resolve/main/model.safetensors" 126return cls .get_list_tensors (url ) 127 128# case 2: model has multiple files 129index_url = f" { cls . BASE_DOMAIN } / { model_id } /resolve/main/model.safetensors.index.json" 130is_multiple_files = cls .check_file_exist (index_url ) 131if is_multiple_files : 132# read the index file 133index_data = cls .get_data_by_range (index_url ,0 ) 134index_str = index_data .decode ('utf-8' ) 135index_json = json .loads (index_str ) 136assert index_json .get ("weight_map" )is not None ,"weight_map not found in index file" 137weight_map = index_json ["weight_map" ] 138# get the list of files 139all_files = list (set (weight_map .values ())) 140all_files .sort ()# make sure we load shard files in order 141# get the list of tensors 142tensors :dict [str ,RemoteTensor ]= {} 143for file in all_files : 144url = f" { cls . BASE_DOMAIN } / { model_id } /resolve/main/ { file } " 145for key ,val in cls .get_list_tensors (url ).items (): 146tensors [key ]= val 147return tensors 148 149raise ValueError ( 150f"No safetensor file has been found for model { model_id } ." 151"If the repo has safetensor files, make sure the model is public or you have a " 152"valid Hugging Face token set in the environment variable HF_TOKEN." 153 ) 154 155@classmethod 156def get_list_tensors (cls ,url :str )-> dict [str ,RemoteTensor ]: 157""" 158Get list of tensors from a remote safetensor file. 159 160Returns a dictionary of tensor names and their metadata. 161Each tensor is represented as a tuple of (dtype, shape, offset_start, size) 162""" 163metadata ,data_start_offset = cls .get_metadata (url ) 164res :dict [str ,RemoteTensor ]= {} 165 166for name ,meta in metadata .items (): 167if name == "__metadata__" : 168continue 169if not isinstance (meta ,dict ): 170raise ValueError (f"Invalid metadata for tensor ' { name } ': { meta } " ) 171try : 172dtype = meta ["dtype" ] 173shape = meta ["shape" ] 174offset_start_relative ,offset_end_relative = meta ["data_offsets" ] 175size = offset_end_relative - offset_start_relative 176offset_start = data_start_offset + offset_start_relative 177res [name ]= RemoteTensor (dtype = dtype ,shape = tuple (shape ),offset_start = offset_start ,size = size ,url = url ) 178except KeyError as e : 179raise ValueError (f"Missing key in metadata for tensor ' { name } ': { e } , meta = { meta } " ) 180 181# order by name (same as default safetensors behavior) 182# ref: https://github.com/huggingface/safetensors/blob/0816a1ae1d6b731cefd67f061d80d1cadd0dd7bb/bindings/python/src/lib.rs#L606 183res = dict (sorted (res .items (),key = lambda t :t [0 ])) 184 185return res 186 187@classmethod 188def get_metadata (cls ,url :str )-> tuple [dict ,int ]: 189""" 190Get JSON metadata from a remote safetensor file. 191 192Returns tuple of (metadata, data_start_offset) 193""" 194# Request first 5MB of the file (hopefully enough for metadata) 195read_size = 5 * 1024 * 1024 196raw_data = cls .get_data_by_range (url ,0 ,read_size ) 197 198# Parse header 199# First 8 bytes contain the metadata length as u64 little-endian 200if len (raw_data )< 8 : 201raise ValueError ("Not enough data to read metadata size" ) 202metadata_length = int .from_bytes (raw_data [:8 ],byteorder = 'little' ) 203 204# Calculate the data start offset 205data_start_offset = 8 + metadata_length 206 207# Check if we have enough data to read the metadata 208if len (raw_data )< 8 + metadata_length : 209raise ValueError (f"Could not read complete metadata. Need { 8 + metadata_length } bytes, got { len ( raw_data ) } " ) 210 211# Extract metadata bytes and parse as JSON 212metadata_bytes = raw_data [8 :8 + metadata_length ] 213metadata_str = metadata_bytes .decode ('utf-8' ) 214try : 215metadata = json .loads (metadata_str ) 216return metadata ,data_start_offset 217except json .JSONDecodeError as e : 218raise ValueError (f"Failed to parse safetensor metadata as JSON: { e } " ) 219 220@classmethod 221def get_data_by_range (cls ,url :str ,start :int ,size :int = - 1 )-> bytes : 222""" 223Get raw byte data from a remote file by range. 224If size is not specified, it will read the entire file. 225""" 226import requests 227from urllib .parse import urlparse 228 229parsed_url = urlparse (url ) 230if not parsed_url .scheme or not parsed_url .netloc : 231raise ValueError (f"Invalid URL: { url } " ) 232 233headers = cls ._get_request_headers () 234if size > - 1 : 235headers ["Range" ]= f"bytes= { start } - { start + size } " 236response = requests .get (url ,allow_redirects = True ,headers = headers ) 237response .raise_for_status () 238 239# Get raw byte data 240return response .content [slice (size if size > - 1 else None )] 241 242@classmethod 243def check_file_exist (cls ,url :str )-> bool : 244""" 245Check if a file exists at the given URL. 246Returns True if the file exists, False otherwise. 247""" 248import requests 249from urllib .parse import urlparse 250 251parsed_url = urlparse (url ) 252if not parsed_url .scheme or not parsed_url .netloc : 253raise ValueError (f"Invalid URL: { url } " ) 254 255try : 256headers = cls ._get_request_headers () 257headers ["Range" ]= "bytes=0-0" 258response = requests .head (url ,allow_redirects = True ,headers = headers ) 259# Success (2xx) or redirect (3xx) 260return 200 <= response .status_code < 400 261except requests .RequestException : 262return False 263 264@classmethod 265def _get_request_headers (cls )-> dict [str ,str ]: 266"""Prepare common headers for requests.""" 267headers = {"User-Agent" :"convert_hf_to_gguf" } 268if os .environ .get ("HF_TOKEN" ): 269headers ["Authorization" ]= f"Bearer { os . environ [ 'HF_TOKEN' ] } " 270return headers 271 272 273@dataclass 274class LocalTensorRange : 275filename :Path 276offset :int 277size :int 278 279 280@dataclass 281class LocalTensor : 282dtype :str 283shape :tuple [int , ...] 284data_range :LocalTensorRange 285 286def mmap_bytes (self )-> np .ndarray : 287return np .memmap (self .data_range .filename ,mode = 'c' ,offset = self .data_range .offset ,shape = self .data_range .size ) 288 289 290class SafetensorsLocal : 291""" 292Read a safetensors file from the local filesystem. 293 294Custom parsing gives a bit more control over the memory usage. 295The official safetensors library doesn't expose file ranges. 296""" 297 298tensors :dict [str ,LocalTensor ] 299 300def __init__ (self ,filename :Path ): 301with open (filename ,"rb" )as f : 302metadata_length = int .from_bytes (f .read (8 ),byteorder = 'little' ) 303file_size = os .stat (filename ).st_size 304if file_size < 8 + metadata_length : 305raise ValueError (f"Could not read complete metadata. Need { 8 + metadata_length } bytes, got { file_size } " ) 306 307metadata_str = f .read (metadata_length ).decode ('utf-8' ) 308try : 309metadata = json .loads (metadata_str ) 310except json .JSONDecodeError as e : 311raise ValueError (f"Failed to parse safetensors metadata as JSON: { e } " ) 312 313data_start_offset = f .tell () 314 315tensors :dict [str ,LocalTensor ]= {} 316for name ,meta in metadata .items (): 317if name == "__metadata__" : 318# ignore metadata, it's not a tensor 319continue 320 321tensors [name ]= LocalTensor ( 322dtype = meta ["dtype" ], 323shape = tuple (meta ["shape" ]), 324data_range = LocalTensorRange ( 325filename , 326data_start_offset + meta ["data_offsets" ][0 ], 327meta ["data_offsets" ][1 ]- meta ["data_offsets" ][0 ], 328 ), 329 ) 330 331# order by name (same as default safetensors behavior) 332# ref: https://github.com/huggingface/safetensors/blob/0816a1ae1d6b731cefd67f061d80d1cadd0dd7bb/bindings/python/src/lib.rs#L606 333self .tensors = dict (sorted (tensors .items (),key = lambda t :t [0 ])) 334 335def __enter__ (self ,* args ,** kwargs ): 336del args ,kwargs # unused 337return self .tensors 338 339def __exit__ (self ,* args ,** kwargs ): 340del args ,kwargs # unused