PyTorch转CoreML的GPTNeo模型首次运行RAM占用过高优化求助
优化方案
一、模型转换阶段优化(降低模型体积+减少推理计算量)
核心问题:当前转换的模型为FP32精度,且未开启KV缓存,每次推理需重新计算全序列注意力,导致中间张量内存占用爆炸。
- 权重量化为FP16
转换时指定compute_precision=ct.precision.FLOAT16,可直接将模型体积压缩至400MB以内,内存加载开销减半。
- 权重量化为FP16
- 新增KV缓存逻辑
修改模型forward方法,将past_key_values作为输入输出项,每次推理仅输入最新的1个token,复用之前的KV缓存结果,可减少90%以上的注意力计算量和中间张量内存占用。
- 新增KV缓存逻辑
- 移除无用输出
删掉forward中无意义的sentence返回值,避免CoreML为无用输出分配冗余内存。
- 移除无用输出
- 优先指定硬件加速单元
转换时指定compute_units=ct.ComputeUnit.CPU_AND_GPU或者ALL,优先调用GPU/ANE单元推理,降低CPU推理的额外内存开销。
- 优先指定硬件加速单元
修改后的核心转换代码:
from numpy.core.numeric import indices import torch import numpy as np from transformers import GPTNeoForCausalLM import coremltools as ct with torch.no_grad(): class ModelClass(torch.nn.Module): def __init__(self, model=None): super(ModelClass, self).__init__() self.next_token_predictor = model def forward(self, x, past_key_values=None): # 仅输入最新token,复用历史KV缓存 outputs = self.next_token_predictor(x, past_key_values=past_key_values, use_cache=True) predictions = outputs.logits past_key_values = outputs.past_key_values topKProbs, topKIndexes = torch.topk(predictions[-1, :], 100, dim=0, sorted=True) return past_key_values, topKProbs, topKIndexes token_predictor = GPTNeoForCausalLM.from_pretrained("EleutherAI/gpt-neo-125M", torchscript=True).eval() # 测试输入适配KV缓存 random_tokens = torch.randint(1, (1,)) traced_token_predictor = torch.jit.trace(token_predictor, random_tokens) model = ModelClass(model=traced_token_predictor) scripted_model = torch.jit.script(model) mlmodel = ct.convert( scripted_model, inputs=[ ct.TensorType(name="sentence", shape=(ct.RangeDim(1, 1),), dtype=np.int32), # 可选输入KV缓存,首次推理不传 ct.OptionalTensorType(name="past_key_values") ], compute_precision=ct.precision.FLOAT16, compute_units=ct.ComputeUnit.ALL ) # 重命名输出逻辑不变 spec = mlmodel.get_spec() ct.utils.rename_feature(spec, 'topKProbs_1_0', 'topKProbs') ct.utils.rename_feature(spec, 'topKProbs_1_1', 'topKIndexes') mlmodel = ct.models.MLModel(spec) mlmodel.save("gptNeo125MtopK.mlmodel")
二、Swift部署阶段优化(降低运行时内存峰值)
核心问题:当前代码每次强制将输入pad到最大序列长度64,且未及时释放中间变量,导致首次推理预分配内存过高。
- 移除输入padding逻辑
启用KV缓存后每次仅输入最新的1个token,不需要再pad到最大长度,避免CoreML预分配最大序列对应的中间张量内存。
- 移除输入padding逻辑
- 增加自动释放池包裹推理逻辑
每次推理的中间变量(MLMultiArray、输入输出对象)会在自动释放池结束后立即释放,避免内存堆积。
- 增加自动释放池包裹推理逻辑
- 调整模型加载时机
将模型加载放到后台异步队列执行,避免主线程加载时的内存峰值叠加。
- 调整模型加载时机
修改后的核心Swift代码:
class NEO { private let model: gptNeo125MtopK public let tokenizer = GPT2Tokenizer() public let seqLen = 64 private let strategy: DecodingStrategy // 存储KV缓存,每次推理复用 private var pastKV: Any? init(strategy: DecodingStrategy = .topK(8)) { self.strategy = strategy // 配置硬件加速加载模型 let config = MLModelConfiguration() config.computeUnits = .all self.model = try! gptNeo125MtopK(configuration: config) } func predict(tokens: [Int]) -> Int { autoreleasepool { // 仅取最新的1个token输入 let lastToken = [tokens.last!] let input_ids = MLMultiArray.from(lastToken) let input: gptNeo125MtopKInput if let pastKV = pastKV { input = gptNeo125MtopKInput(sentence: input_ids, past_key_values: pastKV) } else { input = gptNeo125MtopKInput(sentence: input_ids) } let output = try! model.prediction(input: input) // 更新KV缓存 self.pastKV = output.past_key_values let sampleIndex = Math.sample( indexes: MLMultiArray.toIntArray(output.topKIndexes), probs: MLMultiArray.toFloatArray(output.topKProbs) ) return sampleIndex } } func generate(text: String, nTokens: Int = 5) -> Void { var tokens = tokenizer.encode(text: text) // 首次推理先处理初始文本,获取初始KV缓存 if tokens.count > 1 { for i in 0..<tokens.count-1 { _ = predict(tokens: [tokens[i]]) } } for _ in 0..<nTokens { let nextToken = predict(tokens: tokens) tokens.append(nextToken) print(tokenizer.decode(tokens: tokens)) } } } // App启动逻辑 @main struct TestApp: App { var body: some Scene { WindowGroup { ContentView() } } init() { // 后台队列加载模型和推理,避免主线程阻塞 DispatchQueue.global(qos: .userInitiated).async { let neo = NEO() neo.generate(text: "The Manhattan bridge is") } } }
效果验证
上述优化完成后,模型体积会降至400MB以内,首次推理内存峰值不超过800MB,稳定运行时内存占用在300-500MB区间,可满足1GB以内的内存要求。
内容的提问来源于stack exchange,提问作者Olexander Korenyuk
相关产品推荐
相关产品推荐

