|
| 1 | +# ------------------------------------------------------------------------- |
| 2 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | +# Licensed under the MIT License. |
| 4 | +# -------------------------------------------------------------------------- |
| 5 | + |
| 6 | +""" |
| 7 | +Export LLM to onnx |
| 8 | +""" |
| 9 | +import argparse |
| 10 | +import inspect |
| 11 | +import math |
| 12 | +import os |
| 13 | +import tempfile |
| 14 | +from pathlib import Path |
| 15 | +from typing import Optional |
| 16 | + |
| 17 | +import onnx |
| 18 | +import torch |
| 19 | +import transformers |
| 20 | +from torch import nn |
| 21 | + |
| 22 | + |
| 23 | +def disable_huggingface_init(): |
| 24 | + """do not init model twice as it slow initialization""" |
| 25 | + |
| 26 | + torch.nn.init.kaiming_uniform_ = lambda x, *args, **kwargs: x |
| 27 | + torch.nn.init.uniform_ = lambda x, *args, **kwargs: x |
| 28 | + torch.nn.init.normal_ = lambda x, *args, **kwargs: x |
| 29 | + torch.nn.init.constant_ = lambda x, *args, **kwargs: x |
| 30 | + torch.nn.init.xavier_uniform_ = lambda x, *args, **kwargs: x |
| 31 | + torch.nn.init.xavier_normal_ = lambda x, *args, **kwargs: x |
| 32 | + torch.nn.init.kaiming_normal_ = lambda x, *args, **kwargs: x |
| 33 | + torch.nn.init.orthogonal_ = lambda x, *args, **kwargs: x |
| 34 | + |
| 35 | + |
| 36 | +def get_model_parameter_size(model: nn.Module): |
| 37 | + """to calculate how much memory this model needs""" |
| 38 | + param_size = 0 |
| 39 | + param_sum = 0 |
| 40 | + for param in model.parameters(): |
| 41 | + param_size += param.nelement() * param.element_size() |
| 42 | + param_sum += param.nelement() |
| 43 | + buffer_size = 0 |
| 44 | + buffer_sum = 0 |
| 45 | + for buffer in model.buffers(): |
| 46 | + buffer_size += buffer.nelement() * buffer.element_size() |
| 47 | + buffer_sum += buffer.nelement() |
| 48 | + all_size = (param_size + buffer_size) / 1024 / 1024 |
| 49 | + return all_size |
| 50 | + |
| 51 | + |
| 52 | +def initialize_model_and_sample_inputs(hf_model: str, cache_dir: Optional[str], tokenizer=None): |
| 53 | + """ |
| 54 | + get the pretrained torch model from hugginface, |
| 55 | + and sample model-inputs |
| 56 | + """ |
| 57 | + |
| 58 | + disable_huggingface_init() |
| 59 | + |
| 60 | + model = transformers.AutoModelForCausalLM.from_pretrained( # type: ignore |
| 61 | + hf_model, torch_dtype=torch.float16, cache_dir=cache_dir, trust_remote_code=True |
| 62 | + ) |
| 63 | + if tokenizer is None: |
| 64 | + tokenizer = hf_model |
| 65 | + tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer) # type: ignore |
| 66 | + |
| 67 | + sample_inputs = tuple(tokenizer("Hello, my dog is cute", return_tensors="pt").values()) |
| 68 | + return model, sample_inputs |
| 69 | + |
| 70 | + |
| 71 | +def auto_pipeline_parallel(model: nn.Module, gpulist: list, sample_inputs: tuple): |
| 72 | + """Make the model executable across multiple GPUs.""" |
| 73 | + |
| 74 | + def input_gpu_device_hook(mod, inputs, kwargs): |
| 75 | + modifyed_inputs = [] |
| 76 | + first_dev = None |
| 77 | + for layer_input in inputs: |
| 78 | + if type(layer_input) is not torch.Tensor: |
| 79 | + modifyed_inputs.append(layer_input) |
| 80 | + elif hasattr(mod, "weight"): |
| 81 | + modifyed_inputs.append(layer_input.to(mod.weight.device)) |
| 82 | + elif hasattr(mod, "parameters"): |
| 83 | + device = next(mod.parameters(), layer_input).device |
| 84 | + modifyed_inputs.append(layer_input.to(device)) |
| 85 | + elif hasattr(next(mod.children(), None), "weight"): |
| 86 | + modifyed_inputs.append(layer_input.to(next(mod.children()).weight.device)) |
| 87 | + elif first_dev is not None and layer_input.device != first_dev: |
| 88 | + modifyed_inputs.append(layer_input.to(first_dev)) |
| 89 | + else: |
| 90 | + modifyed_inputs.append(layer_input) |
| 91 | + if first_dev is None: |
| 92 | + first_dev = modifyed_inputs[0].device |
| 93 | + for key, value in kwargs.items(): |
| 94 | + if type(value) is torch.Tensor: |
| 95 | + kwargs[key] = value.to(first_dev) |
| 96 | + |
| 97 | + return (tuple(modifyed_inputs), kwargs) |
| 98 | + |
| 99 | + def move_layer_to_device_rurc(mod, dev): |
| 100 | + mod.to(dev) |
| 101 | + for layer in mod.named_children(): |
| 102 | + move_layer_to_device_rurc(layer[1], dev) |
| 103 | + |
| 104 | + model = model.half() |
| 105 | + all_hooks = [] |
| 106 | + all_hooks.append(model.register_forward_pre_hook(input_gpu_device_hook, with_kwargs=True)) |
| 107 | + pre_fix = next(iter(model.named_children()))[0] |
| 108 | + for top_name, top_module in model.named_children(): |
| 109 | + for name, module in top_module.named_children(): |
| 110 | + all_hooks.append(module.register_forward_pre_hook(input_gpu_device_hook, with_kwargs=True)) |
| 111 | + if type(module) in [torch.nn.ModuleList]: |
| 112 | + num_layers_on_each_gpu = math.floor(len(module) / len(gpulist)) |
| 113 | + for idx, attn_layer in enumerate(module): |
| 114 | + all_hooks.append(attn_layer.register_forward_pre_hook(input_gpu_device_hook, with_kwargs=True)) |
| 115 | + |
| 116 | + to_dev = gpulist[min(idx // num_layers_on_each_gpu, len(gpulist))] |
| 117 | + attn_layer.to(to_dev) |
| 118 | + move_layer_to_device_rurc(attn_layer, to_dev) |
| 119 | + print(f"move {pre_fix}.{name}.{idx} to {to_dev}") |
| 120 | + else: |
| 121 | + module.to(gpulist[0]) |
| 122 | + print(f"move {pre_fix}.{name} to {gpulist[0]}") |
| 123 | + if len(list(top_module.named_children())) == 0: |
| 124 | + top_module.to(gpulist[0]) |
| 125 | + print(f"move {top_name} to {gpulist[0]}") |
| 126 | + |
| 127 | + with torch.no_grad(): |
| 128 | + model(sample_inputs[0], attention_mask=sample_inputs[1]) |
| 129 | + return model |
| 130 | + |
| 131 | + |
| 132 | +def retrieve_onnx_inputs(model: nn.Module, sample_inputs: tuple, with_past: bool): |
| 133 | + """ |
| 134 | + auto retrieve onnx inputs from torch model as we can't enumlate all possibilities |
| 135 | + for all models |
| 136 | + """ |
| 137 | + user_inputs = [] |
| 138 | + |
| 139 | + def hook_for_inputs(_, inputs, kwargs): |
| 140 | + user_inputs.append((inputs, kwargs)) |
| 141 | + return user_inputs[0] |
| 142 | + |
| 143 | + hook_handle = model.register_forward_pre_hook(hook_for_inputs, with_kwargs=True) |
| 144 | + |
| 145 | + forward_params = inspect.signature(model.forward).parameters |
| 146 | + input_keys = list(forward_params.keys()) |
| 147 | + default_values = [forward_params.get(key).default for key in input_keys] |
| 148 | + out = model(sample_inputs[0], attention_mask=sample_inputs[1]) |
| 149 | + hook_handle.remove() |
| 150 | + user_inputs = user_inputs[0] |
| 151 | + onnx_inputs = default_values |
| 152 | + for idx, _val in enumerate(user_inputs[0]): |
| 153 | + onnx_inputs[idx] = user_inputs[0][idx] |
| 154 | + for key, value in user_inputs[1].items(): |
| 155 | + idx = input_keys.index(key) |
| 156 | + onnx_inputs[idx] = value |
| 157 | + for idx, (key, value) in enumerate(zip(input_keys, onnx_inputs)): |
| 158 | + if type(value) is torch.Tensor: |
| 159 | + value.to(model.device) |
| 160 | + # Didn't touch past_key_value now, please change it if you want |
| 161 | + if "use_cache" in key: |
| 162 | + onnx_inputs[idx] = with_past |
| 163 | + |
| 164 | + return input_keys, onnx_inputs, out.past_key_values |
| 165 | + |
| 166 | + |
| 167 | +def move_to_approprate_device(model: nn.Module, sample_inputs_tp: tuple) -> nn.Module: |
| 168 | + """ |
| 169 | + According to the model size, we will upload it to |
| 170 | + CPU if has no GPU or enough GPU memory, |
| 171 | + Single GPU if has only one GPU in local or model size is enough to fit one GPU |
| 172 | + Multiple GPU if there is more than one gpu in local and model is too large |
| 173 | + """ |
| 174 | + total_mem_per_cpu = torch.cuda.get_device_properties(0).total_memory / 1024 / 1024 |
| 175 | + |
| 176 | + print(f"Model_Size = {get_model_parameter_size(model)/1024} GB") |
| 177 | + print(f"total_mem_per_cpu = {total_mem_per_cpu/1024} GB") |
| 178 | + if get_model_parameter_size(model) > total_mem_per_cpu * 0.45: |
| 179 | + device_collection = [torch.device(i) for i in range(torch.cuda.device_count())] |
| 180 | + if len(device_collection) > 1: |
| 181 | + print( |
| 182 | + f"{len(device_collection)} GPUs are used to export onnx, \ |
| 183 | + Please set CUDA_VISIBLE_DEVICES to use specific GPU group" |
| 184 | + ) |
| 185 | + model = auto_pipeline_parallel(model, device_collection, sample_inputs_tp) |
| 186 | + else: |
| 187 | + print("!!!! convert model to float and export onnx using CPU") |
| 188 | + model = model.cpu().float() |
| 189 | + else: |
| 190 | + print("Export model on a single GPU") |
| 191 | + model = model.cuda().half() |
| 192 | + return model |
| 193 | + |
| 194 | + |
| 195 | +def adapt_inputs_to_device(sample_inputs: tuple, device: torch.device) -> tuple: |
| 196 | + """move inputs to device""" |
| 197 | + sample_inputs_ = [] |
| 198 | + for sample_int in sample_inputs: |
| 199 | + if isinstance(sample_int, torch.Tensor): |
| 200 | + sample_inputs_.append(sample_int.to(device)) |
| 201 | + else: |
| 202 | + sample_inputs_.append(sample_int) |
| 203 | + return tuple(sample_inputs_) |
| 204 | + |
| 205 | + |
| 206 | +def fetch_onnx_inputs_outputs_name( |
| 207 | + model: nn.Module, |
| 208 | + onnx_inputs: list, |
| 209 | + torch_input_names: tuple, |
| 210 | + past_key_values: tuple, |
| 211 | + with_past: bool, |
| 212 | + input_with_past: bool, |
| 213 | +): |
| 214 | + """fetch onnx inputs and outputs name""" |
| 215 | + num_of_past_key = 0 |
| 216 | + kv_cache_axis = {0: "batch_size"} |
| 217 | + # try get num_of_past_key and shape of past_key_value |
| 218 | + if past_key_values is not None: |
| 219 | + num_of_past_key = len(past_key_values) |
| 220 | + seq_index = (torch.tensor(past_key_values[0][0].shape) == onnx_inputs[0].shape[-1]).nonzero().view(-1) |
| 221 | + assert seq_index.numel() == 1 |
| 222 | + kv_cache_axis = {0: "batch_size", seq_index.item(): "seq_len"} |
| 223 | + |
| 224 | + if not num_of_past_key: |
| 225 | + num_of_past_key = model.config.num_hidden_layers |
| 226 | + |
| 227 | + onnx_inp_names = ("input_ids", "attention_mask") |
| 228 | + onnx_out_names = ("logits",) |
| 229 | + onnx_dynamic_axes = { |
| 230 | + "input_ids": {0: "batch_size", 1: "seq_len"}, |
| 231 | + "attention_mask": {0: "batch_size", 1: "seq_len"}, |
| 232 | + } |
| 233 | + if input_with_past: |
| 234 | + for i in range(num_of_past_key): |
| 235 | + onnx_inp_names += (f"present_key.{i}",) |
| 236 | + onnx_inp_names += (f"present_values.{i}",) |
| 237 | + |
| 238 | + onnx_dynamic_axes[onnx_inp_names[-1]] = kv_cache_axis |
| 239 | + onnx_dynamic_axes[onnx_inp_names[-2]] = kv_cache_axis |
| 240 | + |
| 241 | + if with_past or input_with_past: |
| 242 | + for i in range(num_of_past_key): |
| 243 | + onnx_out_names += (f"past_key.{i}",) |
| 244 | + onnx_out_names += (f"past_values.{i}",) |
| 245 | + onnx_dynamic_axes[onnx_out_names[-1]] = kv_cache_axis |
| 246 | + onnx_dynamic_axes[onnx_out_names[-2]] = kv_cache_axis |
| 247 | + |
| 248 | + for idx, name in enumerate(torch_input_names): |
| 249 | + if input_with_past: |
| 250 | + if name == "past_key_values": |
| 251 | + onnx_inputs[idx] = past_key_values |
| 252 | + elif name == "attention_mask": |
| 253 | + attn_mask = onnx_inputs[idx] |
| 254 | + onnx_inputs[idx] = torch.cat( |
| 255 | + (attn_mask, torch.ones((attn_mask.shape[0], 1), device=attn_mask.device)), dim=1 |
| 256 | + ) |
| 257 | + elif name == "input_ids": |
| 258 | + input_ids = onnx_inputs[idx] |
| 259 | + onnx_inputs[idx] = input_ids[:, -1:] |
| 260 | + |
| 261 | + return onnx_inp_names, onnx_out_names, onnx_dynamic_axes |
| 262 | + |
| 263 | + |
| 264 | +def do_export_internal(model: nn.Module, onnx_io_tuple: tuple, onnx_inputs: tuple, onnx_path: Path, opset: int): |
| 265 | + """do export with torch.onnx.export""" |
| 266 | + onnx_model_name = onnx_path.name |
| 267 | + onnx_inp_names, onnx_out_names, onnx_dynamic_axes = onnx_io_tuple |
| 268 | + # two step to export onnx |
| 269 | + # 1. export onnx with lots of pieces of weights |
| 270 | + # 2. save all weights to external data |
| 271 | + with tempfile.TemporaryDirectory() as tmpdirname: |
| 272 | + tmp_onnx = os.path.join(tmpdirname, "tmp.onnx") |
| 273 | + |
| 274 | + torch.onnx.export( |
| 275 | + model=model, |
| 276 | + args=tuple(onnx_inputs), |
| 277 | + f=tmp_onnx, |
| 278 | + verbose=False, |
| 279 | + opset_version=opset, |
| 280 | + input_names=onnx_inp_names, |
| 281 | + output_names=onnx_out_names, |
| 282 | + dynamic_axes=onnx_dynamic_axes, |
| 283 | + ) |
| 284 | + |
| 285 | + onnx_path.unlink(missing_ok=True) |
| 286 | + (onnx_path.parent / f"{onnx_model_name}_ext.data").unlink(missing_ok=True) |
| 287 | + |
| 288 | + onnx_model = onnx.load(str(tmp_onnx)) |
| 289 | + onnx.save_model( |
| 290 | + onnx_model, |
| 291 | + str(onnx_path), |
| 292 | + save_as_external_data=(len(os.listdir(tmpdirname)) > 1), |
| 293 | + all_tensors_to_one_file=True, |
| 294 | + location=f"{onnx_model_name}_ext.data", |
| 295 | + size_threshold=1024, |
| 296 | + convert_attribute=False, |
| 297 | + ) |
| 298 | + |
| 299 | + |
| 300 | +@torch.no_grad() |
| 301 | +def export_onnx(hf_model: str, cache_dir: Optional[str], onnx_path_str: str, with_past: bool, opset: int): |
| 302 | + """ |
| 303 | + do export |
| 304 | + model: torch model |
| 305 | + onnx_path: where the onnx model saved to |
| 306 | + sample_inputs_tp: inputs for torch model |
| 307 | + """ |
| 308 | + model, sample_inputs_tp = initialize_model_and_sample_inputs(hf_model, cache_dir) |
| 309 | + |
| 310 | + model = move_to_approprate_device(model, sample_inputs_tp) |
| 311 | + |
| 312 | + sample_inputs = adapt_inputs_to_device(sample_inputs_tp, next(model.parameters()).device) |
| 313 | + |
| 314 | + # input_keys would be usesful if the model has some special inputs |
| 315 | + input_keys, onnx_inputs, past_key_value = retrieve_onnx_inputs(model, sample_inputs, with_past) |
| 316 | + |
| 317 | + onnx_io_tuple = fetch_onnx_inputs_outputs_name(model, onnx_inputs, input_keys, past_key_value, with_past, False) |
| 318 | + |
| 319 | + onnx_model_name = "model.onnx" |
| 320 | + onnx_path: Path = Path(onnx_path_str).absolute() |
| 321 | + if onnx_path.suffix != ".onnx": |
| 322 | + onnx_path = onnx_path / onnx_model_name |
| 323 | + |
| 324 | + do_export_internal(model, onnx_io_tuple, onnx_inputs, onnx_path, opset) |
| 325 | + if not with_past: |
| 326 | + return |
| 327 | + |
| 328 | + onnx_io_tuple = fetch_onnx_inputs_outputs_name(model, onnx_inputs, input_keys, past_key_value, with_past, True) |
| 329 | + |
| 330 | + onnx_model_name = "model_with_past.onnx" |
| 331 | + onnx_path = onnx_path.parent / onnx_model_name |
| 332 | + |
| 333 | + do_export_internal(model, onnx_io_tuple, onnx_inputs, onnx_path, opset) |
| 334 | + |
| 335 | + |
| 336 | +def parse_arguments(): |
| 337 | + """arguments parsing.""" |
| 338 | + parser = argparse.ArgumentParser() |
| 339 | + |
| 340 | + parser.add_argument( |
| 341 | + "-m", |
| 342 | + "--model", |
| 343 | + required=True, |
| 344 | + type=str, |
| 345 | + default=["meta-llama/Llama-2-70b-hf"], |
| 346 | + help="Pre-trained models in huggingface model hub", |
| 347 | + ) |
| 348 | + parser.add_argument( |
| 349 | + "-s", |
| 350 | + "--saved_path", |
| 351 | + required=False, |
| 352 | + type=str, |
| 353 | + default="./onnx_models/", |
| 354 | + help="where the onnx model will be saved", |
| 355 | + ) |
| 356 | + parser.add_argument( |
| 357 | + "--cache_dir", |
| 358 | + required=False, |
| 359 | + type=str, |
| 360 | + default=None, |
| 361 | + help=("cache directy of huggingface, by setting this to avoid useless downloading if you have one"), |
| 362 | + ) |
| 363 | + parser.add_argument( |
| 364 | + "--with_past", |
| 365 | + action="store_true", |
| 366 | + default=False, |
| 367 | + help=("The tool will export onnx without past-key-value by default"), |
| 368 | + ) |
| 369 | + parser.add_argument( |
| 370 | + "--opset", |
| 371 | + required=False, |
| 372 | + type=int, |
| 373 | + default=17, |
| 374 | + help=( |
| 375 | + "the opset to save onnx model, \ |
| 376 | + try to increase it if this opset doens't have new features you want" |
| 377 | + ), |
| 378 | + ) |
| 379 | + return parser.parse_args() |
| 380 | + |
| 381 | + |
| 382 | +if __name__ == "__main__": |
| 383 | + args = parse_arguments() |
| 384 | + |
| 385 | + export_onnx(args.model, args.cache_dir, args.saved_path, args.with_past, args.opset) |
0 commit comments