去年我曾在本地部署过 ChatGLM3-6B,并基于它做了一个 ASR→LLM→TTS 的简单实验。最近突然想深入了解一下大模型的剪枝技术,于是决定拿这个熟悉的模型开刀,用 PyTorch 官方的 torch.nn.utils.prune 库进行一次完整的剪枝实验,并记录下整个过程和发现。大模型的权重文件地址:

Huggingface https://huggingface.co/THUDM/chatglm3-6b

阿里云:https://www.modelscope.cn/models/ZhipuAI/chatglm3-6b

基本思路,先用原始的模型权重文件跑简单对话:

首先配置基础,我用的RTX5080,支持的是sm-120,去年的时候有些torch还不支持,想要知道怎么给   RTX5080配置sm-120的可以去看这篇博文:RTX50系列显卡SM_120 架构兼容性问题-CSDN博客

配置:

torch                     2.11.0.dev20260105+cu128 

pytorch-cuda              12.4                 

transformers              4.38.2                  

首先配置基础:

# ========== 基础配置 ==========
LOCAL_MODEL_PATH = r"C:\Users\k\models\ZhipuAI\chatglm3-6b"  # 你的模型路径
DEVICE = "cuda"                     # 固定为 GPU
QUANTIZE = 0         

模型路径填自己的路径,如果没有下载也可以直接填官方网址会自动下。

然后加载模型,用pytorch的加载方式:

def init_model_and_tokenizer():
    """加载模型和分词器(CPU 优化)"""
    print("正在加载分词器...")
    tokenizer = AutoTokenizer.from_pretrained(
        LOCAL_MODEL_PATH,
        trust_remote_code=True,
        use_fast=True
    )
    
    print("正在加载模型(CPU 模式,可能需要几分钟)...")
    model = AutoModel.from_pretrained(
        LOCAL_MODEL_PATH,
        trust_remote_code=True,
        low_cpu_mem_usage=True,
        torch_dtype=torch.float16,      
    )
    
    model = model.to(DEVICE).eval()
    print("模型加载完成。")
    return tokenizer, model

这里torch_dtype在更高的transformer里面似乎改成了dtype,这里兼容我的transfotmer版本,保留了原写法。这里torch.float16用的16位浮点,是为了减少内存占用。试过,32位时候大概占用内存是25G左右,官方给出的是24G 多点,差不多。16FP的话只占用12G内存,并且可以加快速度效果非常明显。

接下来是简单的问答逻辑界面:

def chat_loop(tokenizer, model):
    """基础对话循环,无system prompt,无记忆"""
    history = []  # 初始为空,不包含任何system消息
    
    print("\n===== 原始模型对话(无预设角色)=====")
    print("输入 'exit' 退出对话\n")
    
    while True:
        user_input = input("👤 你: ").strip()
        if user_input.lower() == "exit":
            print("👋 再见!")
            break
        if not user_input:
            continue
        
        try:
            # 调用 model.chat 生成回复,不传入system参数
            response, history = model.chat(
                tokenizer,
                user_input,
                history=history,
                top_p=0.9,
                temperature=0.8
                # 注意:没有 system 参数
            )
            
            print(f"🤖 模型: {response}\n")
        
        except Exception as e:
            print(f"❌ 出错: {e}")

整体就是这样。模型的对话脚本就好了。

然后是剪枝。官方给出模型有28层,实际用layer输出也是28层对应,索引是0到27。

定义剪枝函数:


def prune_model_layer(model, amount, layer_index=19):
    target_prefix = f"transformer.encoder.layers.{layer_index}."
    parameters_to_prune = []
    for name, module in model.named_modules():
        if isinstance(module, nn.Linear) and name.startswith(target_prefix):
            parameters_to_prune.append((module, 'weight'))
    
    if not parameters_to_prune:
        print(f"警告:未找到层 {layer_index} 中的 Linear 模块")
        return
    
    # 执行全局剪枝
    prune.global_unstructured(
        parameters_to_prune,
        pruning_method=prune.L1Unstructured,
        amount=amount,
    )
    
    # 使剪枝永久化,移除掩码,权重变为普通参数(含零值)
    for module, name in parameters_to_prune:
        prune.remove(module, name)
    
    print(f"层 {layer_index} 剪枝完成,剪枝比例: {amount*100:.1f}%")

这里只试验了一下剪枝第19层,剪枝度为0.5. 

也可以用数组prune_model_index存储层的索引。然后主函数里for循环剪枝。

这里设置的是prune.L1Unsructured剪枝方式,也就是L1非结构化剪枝。L1剪枝对于输出差异大的模型剪枝效果会好一点。

剪枝好的模型放到工作目录文件下:

def save_pruned_model(model, save_path="pruned_model.pth"):
    """保存剪枝后的 state_dict(包含掩码)"""
    torch.save(model.state_dict(), save_path)
    print(f"模型已保存到: {save_path}")

掩码mask是这样一个概念,就是剪枝完之后不直接修改原模型权重,而是生成一个对应的mask矩阵,和原模型的参数矩阵耦合,让某些权重变成0。如果要固定,要调用prune.remove。

测试一下剪枝结果:

def load_pruned_model(model_class, model_path, prune_state_dict_path, device='cpu'):
    """
    加载原始模型结构,然后加载剪枝后的 state_dict
    """
    model = model_class.from_pretrained(
        model_path,
        trust_remote_code=True,
        low_cpu_mem_usage=True,
        torch_dtype=torch.float16,## 16位浮点数,减少内存占用和计算时间
    ).to(device)
    model.load_state_dict(torch.load(prune_state_dict_path, map_location=device))
    return model

def main():
    # 1. 加载原始模型(CPU 上,float32)
    print("正在加载原始模型...")
    model = AutoModel.from_pretrained(
        LOCAL_MODEL_PATH,
        trust_remote_code=True,
        low_cpu_mem_usage=True,
        torch_dtype=torch.float16,  # 加载为 float16 以节省内存
    )
    model.eval()

    # 可选:将模型移至 GPU 以加速剪枝(如果有)
    # device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    # model = model.to(device)

    # 2. 打印剪枝前的参数信息
    total_params_before = sum(p.numel() for p in model.parameters())
    print(f"剪枝前参数量: {total_params_before/1e9:.2f} B")

    # 3. 执行剪枝
    for layer_index in prune_model_index:
        prune_model_layer(model, amount=0.5, layer_index=layer_index)
    

    # 4. 打印剪枝后的参数信息(注意:非结构化剪枝不改变参数量,但部分权重变为零)
    total_params_after = sum(p.numel() for p in model.parameters())
    nonzero_params = sum(torch.count_nonzero(p) for p in model.parameters())
    print(f"剪枝后参数量: {total_params_after/1e9:.2f} B")
    print(f"非零参数数量: {nonzero_params/1e9:.2f} B")
    print(f"实际稀疏度: {1 - nonzero_params/total_params_after:.2%}")

    # 5. 保存剪枝后的模型
    save_pruned_model(model, "chatglm3_pruned.pth")

    # 6. (可选)演示加载剪枝后的模型

输出结果:

(llm_micro) C:\Users\k\LLM_Mciro\build>python model_prune.py
正在加载原始模型...
Loading checkpoint shards: 100%|█████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 23.48it/s]
剪枝前参数量: 6.24 B
层 19 剪枝完成,剪枝比例: 50.0%
剪枝后参数量: 6.24 B
非零参数数量: 6.14 B
实际稀疏度: 1.63%
模型已保存到: chatglm3_pruned.pth

降低了0.1B个参数。

测试输入,我想先测试情感类文本生成,所以问题是:写一首讲爱而不得的小诗。

加载模型的时候,剪枝过的模型的transformer和分词器从原来的地方加载,模型权重从工作目录加载。

LOCAL_MODEL_PATH = r"C:\Users\k\models\ZhipuAI\chatglm3-6b"  # 原始模型路径(用于加载配置和分词器)
PRUNED_MODEL_PATH = "chatglm3_pruned.pth"                    # 剪枝后的权重文件(位于工作目录)
DEVICE = "cuda"
def init_model_and_tokenizer():
    """加载分词器和剪枝后的模型"""
    print("正在加载分词器...")
    tokenizer = AutoTokenizer.from_pretrained(
        LOCAL_MODEL_PATH,
        trust_remote_code=True,
        use_fast=True
    )
    
    print("正在加载剪枝后的模型(CPU 模式,可能需要几分钟)...")
    # 1. 加载原始模型结构(不加载权重,仅用于构建)
    model = AutoModel.from_pretrained(
        LOCAL_MODEL_PATH,
        trust_remote_code=True,
        low_cpu_mem_usage=True,
        torch_dtype=torch.float16,      # CPU 必须用 float16
    )
    
    # 2. 加载剪枝后的权重
    state_dict = torch.load(PRUNED_MODEL_PATH, map_location="cuda", weights_only=True)
    model.load_state_dict(state_dict)
    
    model = model.to(DEVICE).eval()
    print("剪枝模型加载完成。")
    return tokenizer, model

输入写一首讲爱而不得的小诗后,剪枝后的模型在思考时间上明显超过非剪枝模型。

神奇的是,剪枝模型在第一次回答的质量上表现超过了非剪枝模型。

epoch 1
👤 你: 写一首讲爱而不得的小诗
🤖 模型: 爱而不得,心痛难忍,
看着你微笑,心中酸涩难言。
你的眼神,如明媚的春天,
而我却只能,远远地望着。

我们的距离,如海洋般遥远,
你的身影,在我心中飘散。
我想要的,是那份真挚的情感,
却只能,在心里默默思念。

或许有一天,你会有机会明白,
我的心意,早已深入骨髓。
但到那时,你可能已经,
 gone,离开我的世界。

爱而不得,让我孤独难耐,
我只能,将这份爱,藏在心底。
愿你能幸福快乐,找到你心中的他,
而我,也会默默地祝福你,永远。

其余几轮的输出基本水平大概是这样:

epoch 2
🤖 模型: 爱而不得,心伤难愈,
夜夜思量,泪如雨落。

繁星闪烁,似你眼眸,
寂静星空,唤不回的温柔。

情似网,编制的太紧,
心 general,如何破茧成蝶?

情可伤,心可碎,
小草绿,花儿败,

爱而不得,苦涩入喉,
只能沉默,忍受折磨。

望穿秋水,望断春晖,
情爱之间,难以取舍。

只能祝福,愿你幸福,
自己的痛苦,自己承受。

爱而不得,心伤难愈,
夜夜思量,泪如雨落。

出现了一些语法上的问题,但是剪枝与非剪枝的模型的表现相当。

接下来可以从ChatGLM3-6B 的官方github库下载微调的方法,准备之后再做:

zai-org/ChatGLM3: ChatGLM3 series: Open Bilingual Chat LLMs | 开源双语对话语言模型

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐