You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyTorch转CoreML的GPTNeo模型首次运行RAM占用过高优化求助

优化方案

一、模型转换阶段优化(降低模型体积+减少推理计算量)

核心问题:当前转换的模型为FP32精度,且未开启KV缓存,每次推理需重新计算全序列注意力,导致中间张量内存占用爆炸。

    1. 权重量化为FP16
      转换时指定compute_precision=ct.precision.FLOAT16,可直接将模型体积压缩至400MB以内,内存加载开销减半。
    1. 新增KV缓存逻辑
      修改模型forward方法,将past_key_values作为输入输出项,每次推理仅输入最新的1个token,复用之前的KV缓存结果,可减少90%以上的注意力计算量和中间张量内存占用。
    1. 移除无用输出
      删掉forward中无意义的sentence返回值,避免CoreML为无用输出分配冗余内存。
    1. 优先指定硬件加速单元
      转换时指定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,且未及时释放中间变量,导致首次推理预分配内存过高。

    1. 移除输入padding逻辑
      启用KV缓存后每次仅输入最新的1个token,不需要再pad到最大长度,避免CoreML预分配最大序列对应的中间张量内存。
    1. 增加自动释放池包裹推理逻辑
      每次推理的中间变量(MLMultiArray、输入输出对象)会在自动释放池结束后立即释放,避免内存堆积。
    1. 调整模型加载时机
      将模型加载放到后台异步队列执行,避免主线程加载时的内存峰值叠加。

修改后的核心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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 17:15:04