Files
ktransformers/kt-kernel/python/cli/utils/analyze_moe_model_old.py
2026-01-13 14:06:34 +00:00

415 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
分析MoE模型的专家分布和大小
"""
import sys
import json
import hashlib
from pathlib import Path
from collections import defaultdict
try:
from safetensors import safe_open
except ImportError:
print("请先安装safetensors: pip install safetensors")
sys.exit(1)
def _get_cache_file():
"""获取集中式缓存文件路径
所有模型的缓存信息存储在一个文件中
缓存位置:~/.ktransformers/cache/moe_analysis.json
"""
cache_dir = Path.home() / ".ktransformers" / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / "moe_analysis.json"
def _load_all_cache():
"""加载所有缓存数据
返回:
dict: {model_path: cache_data}
"""
cache_file = _get_cache_file()
if not cache_file.exists():
return {}
try:
with open(cache_file, "r") as f:
return json.load(f)
except Exception:
return {}
def _save_all_cache(cache_data):
"""保存所有缓存数据
参数:
cache_data: dict, {model_path: cache_data}
"""
cache_file = _get_cache_file()
try:
with open(cache_file, "w") as f:
json.dump(cache_data, f, indent=2)
except Exception as e:
import warnings
warnings.warn(f"Failed to save MoE cache: {e}")
def _compute_model_fingerprint(model_path):
"""计算模型指纹(用于检测模型是否变化)"""
model_path = Path(model_path)
safetensors_files = sorted(model_path.glob("*.safetensors"))
if not safetensors_files:
return None
# 使用文件列表、文件大小和修改时间作为指纹
fingerprint_data = []
for f in safetensors_files:
stat = f.stat()
fingerprint_data.append({"name": f.name, "size": stat.st_size, "mtime": int(stat.st_mtime)})
# 生成哈希
fingerprint_str = json.dumps(fingerprint_data, sort_keys=True)
return hashlib.md5(fingerprint_str.encode()).hexdigest()
def _load_cache(model_path):
"""加载指定模型的缓存
参数:
model_path: 模型路径
返回:
dict 或 None: 缓存的分析结果
"""
model_path = Path(model_path).resolve()
model_path_str = str(model_path)
all_cache = _load_all_cache()
if model_path_str not in all_cache:
return None
try:
cache_entry = all_cache[model_path_str]
# 验证缓存版本(兼容旧版本)
cache_version = cache_entry.get("cache_version", 0)
if cache_version > 1:
# 版本太新,无法读取
return None
# 验证指纹
current_fingerprint = _compute_model_fingerprint(model_path)
if cache_entry.get("fingerprint") != current_fingerprint:
return None # 模型已变化,缓存失效
return cache_entry.get("result")
except Exception:
return None
def _save_cache(model_path, result):
"""保存指定模型的缓存
参数:
model_path: 模型路径
result: 分析结果
"""
model_path = Path(model_path).resolve()
model_path_str = str(model_path)
try:
fingerprint = _compute_model_fingerprint(model_path)
# 加载所有缓存
all_cache = _load_all_cache()
# 更新此模型的缓存
all_cache[model_path_str] = {
"fingerprint": fingerprint,
"result": result,
"cache_version": 1,
"last_updated": __import__("datetime").datetime.now().isoformat(),
}
# 保存所有缓存
_save_all_cache(all_cache)
except Exception as e:
# 缓存保存失败不影响主流程,但可以记录警告
import warnings
warnings.warn(f"Failed to save MoE cache for {model_path}: {e}")
def _load_model_config(model_path):
"""从 config.json 加载模型配置(参考 convert_cpu_weights.py)
参数:
model_path: 模型路径
返回:
dict 或 None: 包含 num_experts 和 num_layers 的字典,失败返回 None
"""
config_path = model_path / "config.json"
if not config_path.exists():
return None
try:
with open(config_path, "r") as f:
config = json.load(f)
# 支持嵌套的 text_config(参考 convert_cpu_weights.py)
if "text_config" in config:
text_cfg = config["text_config"]
else:
text_cfg = config
# 提取专家数量和层数
num_experts = text_cfg.get("n_routed_experts") or text_cfg.get("num_experts")
num_layers = text_cfg.get("num_hidden_layers")
if num_experts and num_layers:
return {"num_experts": num_experts, "num_layers": num_layers}
except Exception:
pass
return None
def analyze_moe_model(model_path, use_cache=True):
"""
分析MoE模型的专家分布和大小
参数:
model_path: 模型路径(字符串或Path对象)
use_cache: 是否使用缓存(默认True)
返回:
dict: {
'num_experts': 专家数量(按编号计,与层数无关),
'num_layers': 层数,
'single_expert_size_gb': 单个专家在所有层的大小(GB),
'rest_size_gb': 其余部分大小,即非expert部分(GB),
'total_size_gb': 总大小(GB),
'all_experts_size_gb': 所有专家大小(GB),
'cached': 是否从缓存读取
}
如果失败返回 None
"""
model_path = Path(model_path)
if not model_path.exists():
print(f"错误: 路径不存在 {model_path}")
return None
# 尝试加载缓存
if use_cache:
cached_result = _load_cache(model_path)
if cached_result:
cached_result["cached"] = True
return cached_result
# 尝试从 config.json 读取配置(参考 convert_cpu_weights.py)
config_info = _load_model_config(model_path)
# 查找所有safetensors文件
safetensors_files = sorted(model_path.glob("*.safetensors"))
if not safetensors_files:
print(f"错误: 在 {model_path} 中没有找到safetensors文件")
return None
# 统计变量
total_size_bytes = 0
expert_0_weights = defaultdict(lambda: defaultdict(lambda: {"size": 0})) # {layer_idx: {weight_name: size}}
num_layers = 0
num_experts_per_layer = 0
layout = "base" # "base" or "fused"
# 确定主前缀(通常是 'model')
# 有些模型可能有多个前缀(如 model. 和 mtp.),我们只统计主前缀的
main_prefix = None
# 遍历所有文件
for file_path in safetensors_files:
try:
with safe_open(file_path, framework="pt", device="cpu") as f:
# 第一次遍历:确定主前缀和layout(参考 convert_cpu_weights.py)
if main_prefix is None:
prefix_counts = defaultdict(int)
for key in f.keys():
# 检测 fused layout(参考 convert_cpu_weights.py)
if ".mlp.experts." in key and "gate_up" in key:
layout = "fused"
if ".mlp.experts." in key or ".block_sparse_moe.experts." in key:
prefix = key.split(".")[0]
prefix_counts[prefix] += 1
if prefix_counts:
# 选择出现次数最多的前缀作为主前缀
main_prefix = max(prefix_counts.items(), key=lambda x: x[1])[0]
for key in f.keys():
tensor = f.get_tensor(key)
# 计算大小
num_params = 1
for dim in tensor.shape:
num_params *= dim
size_bytes = num_params * tensor.element_size()
total_size_bytes += size_bytes
# 判断是否是expert权重
# 支持多种命名格式:.mlp.experts. 或 .block_sparse_moe.experts.
is_expert_weight = ".mlp.experts." in key or ".block_sparse_moe.experts." in key
if is_expert_weight:
parts = key.split(".")
# 只统计主前缀的专家
if main_prefix and not key.startswith(f"{main_prefix}."):
continue
if "layers" in parts:
layer_idx = int(parts[parts.index("layers") + 1])
num_layers = max(num_layers, layer_idx + 1)
else:
layer_idx = None
if layout == "fused":
# Fused layout: model.layers.{layer}.mlp.experts.{proj}
# 所有专家合并在一个张量里,从 config.json 获取专家数
if config_info and layer_idx is not None:
# 使用 config.json 的信息
num_experts_per_layer = config_info["num_experts"]
# Fused 格式下,张量是 [E, ...] 形状,E 是专家数
# 单个专家大小 = tensor_size / num_experts
single_expert_in_tensor = size_bytes / num_experts_per_layer
weight_name = parts[-1] # gate_up 或 down 等
expert_0_weights[layer_idx][weight_name]["size"] += single_expert_in_tensor
elif "experts" in parts:
expert_idx = int(parts[parts.index("experts") + 1])
num_experts_per_layer = max(num_experts_per_layer, expert_idx + 1)
# 收集所有层的 expert 0 权重大小
if expert_idx == 0 and layer_idx is not None:
weight_name = parts[-2]
expert_0_weights[layer_idx][weight_name]["size"] += size_bytes
except Exception as e:
print(f"读取文件 {file_path.name} 时出错: {e}")
return None
# 优先使用 config.json 的信息(参考 convert_cpu_weights.py)
if config_info:
if num_experts_per_layer == 0:
num_experts_per_layer = config_info["num_experts"]
if num_layers == 0:
num_layers = config_info["num_layers"]
# 计算
# 选择第一个包含专家的层(层号最小)
if not expert_0_weights:
# 没有找到任何专家权重
single_expert_size_bytes = 0
first_expert_layer = None
num_expert_layers = 0
else:
first_expert_layer = min(expert_0_weights.keys())
expert_weights = expert_0_weights[first_expert_layer]
# 1. 单个expert在单层的大小
single_expert_size_bytes = sum(w["size"] for w in expert_weights.values())
# 计算有多少层包含专家
num_expert_layers = len(expert_0_weights)
# 2. 单个expert在所有专家层的大小
single_expert_across_layers_size = single_expert_size_bytes * num_expert_layers
# 3. 所有experts的大小 = 单个expert × 专家层数 × 专家数
all_experts_size = single_expert_size_bytes * num_expert_layers * num_experts_per_layer
# 4. 其余部分 = 总大小 - 所有experts
rest_size = total_size_bytes - all_experts_size
# 转换为GB
result = {
"num_experts": num_experts_per_layer,
"num_layers": num_layers,
"single_expert_size_gb": single_expert_across_layers_size / (1024**3),
"rest_size_gb": rest_size / (1024**3),
"total_size_gb": total_size_bytes / (1024**3),
"all_experts_size_gb": all_experts_size / (1024**3),
"cached": False,
}
# 保存缓存
if use_cache:
_save_cache(model_path, result)
return result
def print_analysis(model_path):
"""打印模型分析结果"""
print(f"分析模型: {model_path}\n")
result = analyze_moe_model(model_path)
if result is None:
print("分析失败")
return
print("=" * 70)
print("MoE模型分析结果")
if result.get("cached"):
print("[使用缓存]")
print("=" * 70)
print(f"模型结构:")
print(f" - 专家数量: {result['num_experts']} (按编号计,每层都有这些experts)")
print(f" - 层数: {result['num_layers']}")
print()
print(f"大小统计:")
print(
f" - 单个专家大小: {result['single_expert_size_gb']:.4f} GB "
f"({result['single_expert_size_gb']/result['total_size_gb']*100:.2f}%)"
)
print(
f" - 其余部分大小 (非expert): {result['rest_size_gb']:.4f} GB "
f"({result['rest_size_gb']/result['total_size_gb']*100:.2f}%)"
)
print(f" - 总大小: {result['total_size_gb']:.4f} GB")
print()
print(f"详细分解:")
print(f" - 非Expert部分: {result['rest_size_gb']:.4f} GB")
print(f" - 所有Experts ({result['num_experts']}个): {result['all_experts_size_gb']:.4f} GB")
print(f" 其中单个expert: {result['single_expert_size_gb']:.4f} GB")
print(
f" 其他{result['num_experts']-1}个experts: {result['all_experts_size_gb'] - result['single_expert_size_gb']:.4f} GB"
)
print("=" * 70)
print()
def main():
# 测试两个模型
models = ["/mnt/data2/models/Qwen3-30B-A3B", "/mnt/data2/models/Qwen3-235B-A22B-Instruct-2507"]
if len(sys.argv) > 1:
# 如果提供了命令行参数,使用它
models = [sys.argv[1]]
for model_path in models:
print_analysis(model_path)
if __name__ == "__main__":
main()