
在大模型本地化应用的浪潮中,硬件门槛高始终是阻挡在我们面前的一道鸿沟,动辄数十 GB 的模型参数文件,足以让多数个人电脑的磁盘捉襟见肘,更遑论显存不足导致的加载失败。为了破解这一难题,模型分片存储与按需加载成为大模型本地部署磁盘空间优化的核心方案。
今天我们以本地现有的模型(大家可以根据实际下载模型)为实操案例,从基础的核心概念拆解入手,一步步厘清分片存储物理切分、索引记录的底层逻辑,以及按需加载延迟调用、动态卸载的运行原理。通过兼容性的分片脚本,实现模型的按量切片,切片后通过完整性校验、加载测试、推理验证确保分片后的模型既能节省磁盘与显存资源,又能保持与完整模型一致的运行效果。

大模型的体型非常庞大,比如 LLaMA 2 70B 模型,完整参数文件的大小超过130GB;GPT-3 的参数规模更是达到 1750 亿,完整存储需要数百 GB 甚至 TB 级别的磁盘空间。而普通个人电脑的固态硬盘容量通常在 512GB~2TB 之间,单独存放一个大模型就会占用大量空间,更别说同时部署多个模型或运行其他应用了。
把大模型的完整参数文件,切成若干个小的分片文件,就像把一整块蛋糕切成小块,分别放在不同的盘子里。每个分片的大小可以根据你的磁盘剩余空间灵活设置,比如切成 10GB 一个的分片。这些分片既可以存在同一台电脑的不同磁盘分区,也可以存在局域网内的其他设备,比如 NAS、另一台电脑。
大模型运行时,并不是所有参数都需要同时加载到内存和显存中。按需加载就是用哪个分片,就临时加载哪个分片,不用的分片仍然存在磁盘上,等需要的时候再调用。就像看书时,你不会把整本书都捧在手里,而是翻到哪一页就看哪一页,看完这页再翻下一页。
总体来说:分片存储是物理上切分文件,解决磁盘空间不足的问题;按需加载是逻辑上动态调用,解决内存或显存不足的问题,两者结合是大模型本地部署的空间优化黄金组合。
要搞懂分片和按需加载,需要先明白两个大模型运行的基础常识:
1. 大模型的参数存储形式
大模型的参数本质上是海量的数值矩阵,这些矩阵以二进制文件的形式保存在磁盘上,常见的格式有:
这些参数文件是模型记住知识的载体,比如一个 7B 模型的.bin 文件,就是包含 70 亿个参数数值的集合。
2. 大模型的运行流程:加载 - 计算 - 输出
大模型处理一个用户请求(比如 “写一篇短文”)的流程是:
其中,第一步“加载参数”是最消耗空间的环节,而按需加载就是在这一步做优化。
3. 关键限制因素
分片存储解决磁盘的痛点,按需加载解决内存和显存的痛点。
分片存储的本质是“分块存储 + 索引记录”,分为两个步骤:
工具(比如 Hugging Face 的transformers库、safetensors工具)会按照我们设定的分片大小,把完整的参数矩阵切成多个连续的子矩阵。
切分完成后,工具会自动生成一个索引文件,比如model.safetensors.index.json,这个文件的作用是记录每个分片包含哪些参数、存放在哪个路径。
核心关键点:分片只是物理切分,模型的参数逻辑结构没有变,就像把一串糖葫芦拆成几串,每颗山楂的顺序和味道都没变。
按需加载的核心是“延迟加载 + 动态卸载”,依赖大模型的分层次计算特性:
基于这个特性,按需加载的流程是:
通俗理解:这就像工厂的流水线,只有当前工序需要的零件才会被送到工位上,用完就撤走,工位上永远只留正在加工的零件,节省了工位空间。
两者结合时,整个流程就像 “按清单取货,用完即还”:
我们以Qwen1.5-1.8B-Chat模型为例,使用Hugging Face transformers+safetensors工具,讲解分片存储和按需加载的完整执行流程。

核心步骤:
from safetensors.torch import save_file, load_file
import os
import shutil
# ===================== 配置参数 =====================
input_model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat"
output_model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat-split"
# 手动设置分片数量(1.8B模型设为2即可)
shard_num = 2
# ===================== 核心分片逻辑 =====================
def split_qwen_model_compatible():
# 1. 创建输出目录
os.makedirs(output_model_dir, exist_ok=True)
# 2. 复制必需的配置文件(关键步骤)
required_files = [
"config.json", "tokenizer_config.json",
"vocab.json", "merges.txt", "generation_config.json"
]
for file_name in required_files:
src_path = os.path.join(input_model_dir, file_name)
dst_path = os.path.join(output_model_dir, file_name)
if os.path.exists(src_path):
shutil.copy(src_path, dst_path)
print(f"✅ 复制配置文件:{file_name}")
else:
print(f"⚠️ 缺失配置文件:{file_name}")
# 3. 找到原模型的主参数文件
main_param_file = None
for file in os.listdir(input_model_dir):
if file.endswith(".safetensors") and "model" in file:
main_param_file = os.path.join(input_model_dir, file)
break
if not main_param_file:
raise FileNotFoundError("未找到 .safetensors 格式的参数文件")
# 4. 加载完整模型参数
print(f"正在加载模型参数:{main_param_file}")
model_params = load_file(main_param_file)
param_keys = list(model_params.keys())
# 计算每个分片需要存放的参数数量
keys_per_shard = len(param_keys) // shard_num
# 5. 手动切分并保存分片文件
index_data = {"metadata": {}, "weight_map": {}}
for i in range(shard_num):
# 切分参数字典
start_idx = i * keys_per_shard
end_idx = start_idx + keys_per_shard if i != shard_num -1 else len(param_keys)
shard_keys = param_keys[start_idx:end_idx]
shard_params = {k: model_params[k] for k in shard_keys}
# 定义分片文件名(遵循transformers标准命名)
shard_name = f"model-{i+1:05d}-of-{shard_num:05d}.safetensors"
shard_path = os.path.join(output_model_dir, shard_name)
# 保存当前分片
save_file(shard_params, shard_path)
print(f"✅ 生成分片文件:{shard_name}")
# 填充索引文件数据
for k in shard_keys:
index_data["weight_map"][k] = shard_name
# 6. 手动生成索引文件(关键!框架靠它识别分片)
index_path = os.path.join(output_model_dir, "model.safetensors.index.json")
import json
with open(index_path, "w", encoding="utf-8") as f:
json.dump(index_data, f, indent=2)
print(f"✅ 生成索引文件:model.safetensors.index.json")
# ===================== 执行脚本 =====================
if __name__ == "__main__":
try:
split_qwen_model_compatible()
except Exception as e:
print(f"❌ 分片失败:{str(e)}") 输出结果:
✅ 复制配置文件:tokenizer_config.json ✅ 复制配置文件:vocab.json ✅ 复制配置文件:merges.txt ✅ 复制配置文件:generation_config.json 正在加载模型参数:D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat\model.safetensors ✅ 生成分片文件:model-00001-of-00002.safetensors ✅ 生成分片文件:model-00002-of-00002.safetensors ✅ 生成索引文件:model.safetensors.index.json
执行完成后,Qwen1___5-1___8B-Chat-split目录下会生成多个分片文件和一个索引文件,比如:
Qwen1___5-1___8B-Chat-split/ ├── model-00001-of-00002.safetensors # 分片1 ├── model-00002-of-00002.safetensors # 分片2 └── model.safetensors.index.json # 索引文件
Qwen1___5-1___8B-Chat-split 目录下应该有以下文件,文件都对应上才算正常:
Qwen1___5-1___8B-Chat-split/ ├── config.json # 模型配置文件 ├── tokenizer_config.json # 分词器配置 ├── vocab.json # 词表文件 ├── merges.txt # 分词器合并规则 ├── generation_config.json # 生成配置 ├── model-00001-of-00002.safetensors # 分片1 ├── model-00002-of-00002.safetensors # 分片2 └── model.safetensors.index.json # 分片索引文件(核心)
验证分片文件和索引文件是否损坏,避免加载时因文件问题报错。
import json
import os
# 配置分片模型路径
model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat-split"
def check_shard_integrity():
# 1. 检查索引文件是否存在
index_path = os.path.join(model_dir, "model.safetensors.index.json")
if not os.path.exists(index_path):
return False, "❌ 缺失分片索引文件 model.safetensors.index.json"
# 2. 读取索引文件,校验分片路径
with open(index_path, "r", encoding="utf-8") as f:
index_data = json.load(f)
shard_files = set()
for _, shard_path in index_data["weight_map"].items():
shard_files.add(shard_path)
# 3. 检查所有分片文件是否存在
for shard in shard_files:
shard_full_path = os.path.join(model_dir, shard)
if not os.path.exists(shard_full_path):
return False, f"❌ 缺失分片文件:{shard}"
# 4. 检查配置文件完整性
required_configs = ["config.json", "tokenizer_config.json", "generation_config.json"]
for cfg in required_configs:
if not os.path.exists(os.path.join(model_dir, cfg)):
return False, f"❌ 缺失配置文件:{cfg}"
return True, "✅ 所有分片文件和配置文件完整!"
# 执行校验
is_valid, msg = check_shard_integrity()
print(msg)输出结果:
✅ 所有分片文件和配置文件完整!
验证 transformers 框架能否正常识别分片,加载过程中是否有报错,核心是测试 索引文件有效性 和 分片加载逻辑。
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat-split"
def test_shard_loading():
try:
# 1. 加载配置文件(先不加载参数,快速验证配置)
config = AutoConfig.from_pretrained(model_dir)
print(f"✅ 配置文件加载成功,模型维度:{config.hidden_size}")
# 2. 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
print(f"✅ 分词器加载成功,词表大小:{len(tokenizer)}")
# 3. 加载分片模型(关键:device_map='auto' 启用按需加载)
model = AutoModelForCausalLM.from_pretrained(
model_dir,
device_map="cpu", # 自动分配CPU/GPU显存,实现分片按需加载
low_cpu_mem_usage=True,
trust_remote_code=True
)
print(f"✅ 分片模型加载成功,模型设备:{model.device}")
return True
except Exception as e:
print(f"❌ 模型加载失败:{str(e)}")
return False
# 执行加载测试
test_shard_loading()输出结果:
✅ 所有分片文件和配置文件完整! ✅ 配置文件加载成功,模型维度:2048 ✅ 分词器加载成功,词表大小:151646 ✅ 分片模型加载成功,模型设备:cpu
验证分片模型的生成效果是否与完整模型一致,同时测试响应速度和资源占用。
from transformers import AutoModelForCausalLM, AutoTokenizer
import time
model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat-split"
# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_dir,
device_map="auto",
low_cpu_mem_usage=True,
trust_remote_code=True
)
# 测试案例
test_prompts = [
"你好,请介绍一下自己",
"解释什么是大模型分片存储",
"写一段关于人工智能的短文(50字以内)"
]
# 执行推理
for i, prompt in enumerate(test_prompts):
print(f"\n===== 测试案例 {i+1} =====")
# 构造Qwen的对话格式
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 计时推理过程
start_time = time.time()
inputs = tokenizer([text], return_tensors="pt").to(model.device)
outputs = model.generate(
**inputs,
max_new_tokens=150,
temperature=0.7,
do_sample=True
)
end_time = time.time()
# 解码输出
response = tokenizer.decode(outputs[0], skip_special_tokens=True).split("assistant\n")[-1]
print(f"用户输入:{prompt}")
print(f"模型回复:{response}")
print(f"响应耗时:{end_time - start_time:.2f} 秒")输出结果:
===== 测试案例 1 ===== 用户输入:你好,请介绍一下自己 模型回复:您好!我是一个由阿里云开发的超大规模语言模型,名叫通义千问。我被设计用来回答各种问题和提供信息,包括但不限于科技、文化、生活、历史、科学等多个领域。 我可以理解自然语言文本,并通过深度学习算法从大量的语料库中提取知识和模式,生成准确、流畅的回答。我的回答基于对大量文本数据的学习,以及对语言学、人工智能等相关领域的深入理解和分析。我能够处理复杂的问题,具有良好的推理能力和逻辑思维能力,可以进行问答、提供建议、创作文字等任务。 在训练过程中,我会不断优化自身的语言模型,提升其表达能力、泛化能力和适应性,以满足用户的不同需求。同时 响应耗时:56.05 秒 ===== 测试案例 2 ===== 用户输入:解释什么是大模型分片存储 模型回复:在大数据处理和机器学习领域,"大模型分片存储"是一种将大型深度学习模型分割成多个小块,每个小块独立进行训练和存储的方法。这种技术主要用于解决传统分布式存储系统中由于大规模模型的并行计算和数据密集性导致的性能瓶颈问题。 具体来说,大模型通常包含大量的参数、特征和计算量,这些参数和特征往往分布在多个不同的部分或维度上,如卷积层、全连接层、循环神经网络等。这些部分之间的依赖关系复杂,如果直接在单个存储节点上进行并行计算和存储,可能会导致内存不足、计算资源浪费等问题。此外,当模型规模增大时,其计算任务也会变得 响应耗时:54.09 秒 ===== 测试案例 3 ===== 用户输入:写一段关于人工智能的短文(50字以内) 模型回复:人工智能是一种模拟人类智能的技术,它通过算法和机器学习技术,让计算机系统具有自主思考、学习和决策的能力。它广泛应用于自动驾驶、语音识别、图像处理、医疗诊断等领域,正在改变我们的生活方式和社会发展。 响应耗时:18.16 秒
验证分片按需加载是否真的节省显存和内存,对比完整模型的资源占用差异。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_dir = r"D:\modelscope\hub\qwen\Qwen1___5-1___8B-Chat-split"
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
model_dir,
device_map="auto",
low_cpu_mem_usage=True,
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
# 测试显存占用
def test_resource_usage():
# 空输入时的基础显存占用
if torch.cuda.is_available():
base_memory = torch.cuda.memory_allocated() / 1024 / 1024
print(f"✅ GPU基础显存占用:{base_memory:.2f} MB")
# 推理时的峰值显存占用
prompt = "写一个大模型本地部署的教程大纲"
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
outputs = model.generate(**inputs, max_new_tokens=200)
if torch.cuda.is_available():
peak_memory = torch.cuda.max_memory_allocated() / 1024 / 1024
print(f"✅ 推理峰值显存占用:{peak_memory:.2f} MB")
else:
print("✅ 使用CPU推理,无显存占用")
test_resource_usage()当大模型分片文件存放在不同位置,通常是同一台电脑的不同磁盘分区时,核心配置思路是修改分片索引文件的路径映射、确保加载框架能访问到所有分片位置,以分片存在同一台电脑的不同磁盘分区为例:
比如:
配置步骤:
确保配置文件(config.json等)和索引文件(model.safetensors.index.json)放在同一个目录下,分片文件分散在不同分区,结构如下:
D:\qwen-split-config\ ├── config.json ├── tokenizer_config.json ├── vocab.json └── model.safetensors.index.json # 核心:修改这个文件的路径映射 D:\model_shards\shard1\ └── model-00001-of-00002.safetensors E:\model_shards\shard2\ └── model-00002-of-00002.safetensors
索引文件里的weight_map字段,原本记录的是分片文件名,现在要改成分片的绝对路径,让框架能精准找到每个分片。
{
"metadata": {},
"weight_map": {
"model.embed_tokens.weight": "D:\\model_shards\\shard1\\model-00001-of-00002.safetensors",
"model.layers.0.self_attn.q_proj.weight": "D:\\model_shards\\shard1\\model-00001-of-00002.safetensors",
// ... 其他参数映射
"model.norm.weight": "E:\\model_shards\\shard2\\model-00002-of-00002.safetensors",
"lm_head.weight": "E:\\model_shards\\shard2\\model-00002-of-00002.safetensors"
}
}加载时指定配置文件 + 索引文件所在的目录即可,框架会根据索引文件里的绝对路径去对应分区找分片:
from transformers import AutoModelForCausalLM, AutoTokenizer
# 指向配置文件和索引文件的目录
model_config_dir = r"D:\qwen-split-config"
tokenizer = AutoTokenizer.from_pretrained(model_config_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_config_dir,
device_map="auto",
low_cpu_mem_usage=True,
trust_remote_code=True
)
# 测试推理
prompt = "你好,详细介绍你自己"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))通过这些我们了解了普通电脑怎么通过分片存储和按需加载,轻松跑起来 Qwen1.5-1.8B-Chat 大模型,解决磁盘、显存不够用的问题。核心就是把大模型拆成小分片,像切蛋糕似的分开存,再用到哪个分片就加载哪个,不用一次性装完整个模型。
要是分片想存在不同磁盘,或者连 NAS、其他电脑,改改索引文件里的路径,同机就写绝对路径,局域网就把共享目录挂成本地盘符就行。最后咱们还做了三层测试,确认文件没少、能正常加载、推理效果和完整模型一样。
总的来说,这套方法不用高端电脑,对部署大模型又有了新的扩展和选择,实操时可能会遇到各种版本兼容性问题,交给AI编程工具可以协作我们解决,同时运行过程种要注意路径和权限问题,逐步摸索,现在各种智能工具也大大降低了我们大模型本地使用的门槛。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。