论文: Pruner-Zero: Evolving Symbolic Pruning Metric from scratch for Large Language Models
github地址:https://github.com/pprp/Pruner-Zero
核心idea
- 自适应求解权重重要性函数方程式
- f通过基于符号回归与基因编码树的方法进行求解
实验
需要注意的是,Pruner-Zero需要先通过一遍反向传播来计算梯度G(保存为pth文件),最后再将其作为输入进行剪枝(用作权重重要性的参数G)
所以在复现代码时
- 首先要复现Pruner-Zero项目下的梯度计算代码-->lib/gradient_computation.py
- 其次再通过运行main.py,来计算输入输出的X,W-->进行正向传播计算,结合G进行计算W_metric进行权重重要性计算并剪枝-->lib/prune.py
- 最后再通过测试集进行eval计算ppl(困惑度)-->lib/eval.py
Pruner-Zero:
参数 | value |
---|---|
seed | 0 |
nsamples | 128 |
sparsity_ratio | 0.5 |
sparsity_type | unstructured |
prune_method | pruner-zero |
cache_dir | llm_weights |
save | out/{model_name}/{sparsity_type}/{prune_method}/ out/llama2_7b/unstructured/pruner-zero/ |
gradient_path | ./gradients/llama2/gradients_aggregrate_norm_l2_model_llama-2-7b_128_0.pth |
梯度计算-->lib/gradient_computation.py
-
先看运行代码需要的参数
parser = argparse.ArgumentParser() parser.add_argument( '--nsamples', type=int, default=128, help='no of samples used') parser.add_argument( '--scale', type=int, default=100, help='no of samples used') parser.add_argument( '--llama_version', type=int, default=2, help='llama version used') parser.add_argument('--model', type=str, help='model to used', default='../llm_weights/models--meta-llama--Llama-2-7b-hf/snapshots/llama-2-7b') parser.add_argument( '--task', type=str, default='gradient', help='task to be performed') parser.add_argument('--seed', type=int, default=0, help='seed used') args = parser.parse_args()
- nsamples: 是校验数据集的样本量,在论文里为128-->可改
- scale: 计算梯度的缩放系数,默认为100
- llama_version: 1或者2,保存llama模型(包括llama或者llama2)梯度pth的文件名所用到的,对于opt模型(此参数无用)
- model:模型文件路径,建议将下载好的模型保存到本地,这里将模型保存到项目文件夹下的llm_weights,需要注意的是对于缓存模型(比如之前用AutoModelForCausalLM.from_pretrained得到的缓存模型文件,需要将路径追溯到snapshps/xxx(哈希编号),将xxx令取名为模型名如llama-2-7b,方便后面读取)
- task: 直接默认为gradient就好-->计算梯度
-
再看校验数据加载-->他这里加载的是wikitext2数据,是根据自己的本地数据路径进行加载的
def get_wikitext2(nsamples, seed, seqlen, tokenizer): # Load train and test datasets # Load local dataset traindata = load_from_disk('./data/wikitext2_train') # 需要根据自己的路径修改,一般用load_dataset去加载缓存数据 testdata = load_from_disk('./data/wikitext2_test') # 需要根据自己的路径修改,一般用load_dataset去加载缓存数据 # traindata = load_dataset('../data/wikitext2')['train'] # testdata = load_dataset('../data/wikitext2')['test'] # Encode datasets trainenc = tokenizer(' '.join(traindata['text']), return_tensors='pt') testenc = tokenizer('\n\n'.join(testdata['text']), return_tensors='pt') # Generate samples from training set random.seed(seed) trainloader = [] for _ in range(nsamples): i = random.randint(0, trainenc.input_ids.shape[1] - seqlen - 1) j = i + seqlen inp = trainenc.input_ids[:, i:j] tar = inp.clone() # tar[:, :-1] = -100 trainloader.append((inp, tar)) return trainloader, testenc
-
在get_wikitext2()函数修改
traindata = load_from_disk('./data/wikitext2_train') testdata = load_from_disk('./data/wikitext2_test')
- 根据自己的缓存好的数据路径修改为:本文的wikitext2数据路径在Pruer-Zero项目下的data目录中
traindata = load_dataset('../data/wikitext2')['train'] testdata = load_dataset('../data/wikitext2')['test']
-
参数设置完成后,同时数据路径修改完毕后就可以直接在Pruner-Zero的项目下的lib目录下运行代码
- 运行代码
cd ./lib
python gradient_computation.py
# 当然也可以通过调用参数法进行运行,或
python gradient_computation.py --nsamples 128 --llama_version 2 --model /your/model/path --task gradient
-
运行结束后:会将梯度文件xxx.pth保存到,lib目录下的gradients/llama{args.llama_version}/xxx.pth
- 建议可以将lib目录下的gradients目录文件,将其移动到Pruner-Zero项目下-->即./gradients
-
当然如果想要生成的gradients/llama{args.llama_version}/xxx.pth文件在Pruner-Zero项目下可以更改gradient_computation.py代码
- 将with open (f'./gradients/xxx', ...) 改成with open(f'../gradients/xxx', ...)
- 这样生成的目录就到上一层级目录下,即Pruner-Zero项目下
生成的梯度文件如下
llama2-13b-->计算梯度需要2块A100-->大概需要120个g的显存-->运行时间3个小时
权重重要性计算-->剪枝-->main.py
- 事先在项目下./gradients已经准备好了梯度文件
- 在./data准备好了数据加载文件
- 在./llm_weights准备好了模型加载文件
先看代码运行参数
parser = argparse.ArgumentParser()
parser.add_argument('--model', type=str, help='LLaMA model',
default='./llm_weights/models--meta-llama--Llama-2-13b-hf/snapshots/llama-2-13b')
parser.add_argument('--seed', type=int, default=0, help='Seed for sampling the calibration data.')
parser.add_argument('--nsamples', type=int, default=128, help='Number of calibration samples.')
parser.add_argument('--sparsity_ratio', type=float, default=0.5, help='Sparsity level')
parser.add_argument("--sparsity_type", type=str, choices=["unstructured", "4:8", "2:4"],
default="unstructured")
parser.add_argument("--prune_method", type=str, choices=["magnitude", "wanda", "sparsegpt",
"ablate_mag_seq", "ablate_wanda_seq", "ablate_mag_iter",
"ablate_wanda_iter", "search", "pruner-zero", "ablate_prunerzero_seq", "ablate_prunerzero_iter"],
default="pruner-zero")
parser.add_argument("--cache_dir", default="llm_weights", type=str )
parser.add_argument('--use_variant', action="store_true", help="whether to use the wanda variant described in the appendix")
parser.add_argument('--save', type=str,
default='out/llama2_13b/unstructured/pruner-zero/',
help='Path to save results.')
parser.add_argument('--save_model', type=str, default=None, help='Path to save the pruned model.')
# gradient_path
parser.add_argument("--gradient_path", type=str,
default='./gradients/llama2/gradients_aggregrate_norm_l2_model_llama-2-13b_128_0.pth',
help="Path to save the gradient.")
parser.add_argument("--json_tree", type=str, default="data/best_tree.json", help="Path to load the json tree.")
parser.add_argument("--eval_zero_shot", action="store_true")
args = parser.parse_args()
- model:模型文件路径,建议将下载好的模型保存到本地,这里将模型保存到项目文件夹下的llm_weights,需要注意的是对于缓存模型(比如之前用AutoModelForCausalLM.from_pretrained得到的缓存模型文件,需要将路径追溯到snapshps/xxx(哈希编号),将xxx取名为模型名如llama-2-13b,方便后面读取)
- seed:随机种子-->0
- nsamples: 是校验数据集的样本量,在论文里为128
- sparsity-ratio: 稀疏率-->0.5(非结构化可调,n:m时为0.5)
- sparsity_type: 稀疏类型-->unstructured
- prune_method: 剪枝类型-->pruner-zero
- cache_dir-->模型缓存路径-->llm_weights
- save-->剪枝后计算ppl保存运行结果路径-->out/{model_name}/{sparsity_type}/{prune_method}/-->out/llama2_13b/unstructured/pruner-zero/
- gradient_path: 梯度加载路径-->./gradients/llama2/gradients_aggregrate_norm_l2_model_llama-2-13b_128_0.pth
修改./lib/data下的get_wikitext2,以及get_c4的数据加载代码,与gradient_computation的修改数据代码是类似的
get_wikitext2修正前
traindata = load_from_disk('./data/wikitext2_train')
testdata = load_from_disk('./data/wikitext2_test')
get_wikitext2修正后
traindata = load_dataset('./data/wikitext2')['train']
testdata = load_dataset('./data/wikitext2')['test']
其他代码记录
lora_ft: 相关代码记录与说明-->lora_ft/finetu_lm.py (wanda也有)
- requirements: peft-->0.7.1,transformers->4.28.0
- 虚拟环境版本过高-->peft->0.11.1, transformers->4.42.4
出现报错信息
把代码中的prepare_model_for_int8_training换成prepare_model_for_kbit_training即可
from peft import prepare_model_for_kbit_training as prepare_model_for_int8_training
附加: 其他知识点
- @dataclass用法:
-
dataclass是类的一个装饰器,只需要在类的前面加入@dataclasss
-
它可以自动生成一些方法,无需手动实现如
- __ init __
- __ repr __
-
举例说明:
@dataclass class Person(): name = "xionglang" pwd : str
- 上述代码中的name-->类属性,在所有方法之外(包括__ init __ ),共享
- pwd-->类实例属性,尽管,没有__ init __初始化,但由于@dataclass装饰器的存在会将其初始化,因此再创建Person类的实例时,必须提供pwd的参数,但是无需给name传参
- 如xl = Person(pwd='123') or xl = Person('123'), 直接写xl = Person()或者传入name参数xl=Person(name='XL)会报错
-
自动设置属性
-
通过dataclass装饰器,实例变量在
__init__
方法中初始化为属性 -
可以通过field(init=False)使得实例变量不被初始化
-
可以通过定义__post_init__方法对field的实例变量进行初始化
-
举例说明:
@dataclass class Person(): name = "xionglang" pwd : str age: int is_adult: bool = field(init=False) # def __post_init__(self): # if self.age>= 18: # self.is_adult = True # else: # self.is_adult = False
-
上面的代码xl = Person(pwd='123', age='20')实例化类后,打印xl.is_adult会报错
-
但是定义__post_init__方法后
@dataclass class Person(): name = "xionglang" pwd : str age: int is_adult: bool = field(init=False) def __post_init__(self): if self.age>= 18: self.is_adult = True else: self.is_adult = False
- 再次调用xl.is_adult则会打印True,从而达到添加is_adult属性的目的
-
-
-
- 静态方法:@staticmethod, 类方法@classmethod
-
静态方法与类或实例无关,它只是类定义体中的一个普通函数
-
静态方法不需要传递类实例(self)
@dataclass class Person(): name = "xionglang" pwd : str age: int is_adult: bool = field(init=False) def __post_init__(self): if self.age>= 18: self.is_adult = True else: self.is_adult = False @staticmethod def publish_paper(paper): return paper+1
- publish_paper是一个静态方法,使用xl.publish_paper(paper)进行调用(注意类里面的方法publish_paper没有写self),只需要在前面添加@staticmethod
-
-
类方法是绑定到类而不是类实例的方法
@dataclass class Person(): name = "xionglang" pwd : str age: int is_adult: bool = field(init=False) def __post_init__(self): if self.age>= 18: self.is_adult = True else: self.is_adult = False @staticmethod def publish_paper(paper): return paper+1 @classmethod def get_name(cls)->str: # 类属性 return cls.name @classmethod def get_pwd(cls)->str: # 实例属性-->报错 return cls.pwd
如果调用xl.get_name会正确返回,而调用xl.get_pwd则会报错,因为前者是类属性(方法外),后者是实例化(__ init __)属性(方法内)
-
- 类方法可以访问和修改类状态。
cls
是一个约定俗成的名称 用来代表类本身,而不是类的实例- 类方法的参数是cls不是self,因为它是调用类本身,而不是类实例
- 类方法传入的 cls 是类本身而不是类的实例化
- 类方法的第一个参数是类本身(cls)
记录一个问题:当在本地项目远程连接服务器时,本地更改的文件上传到远程,被拒绝-->需要对远程文件或目录添加权限,但是如果改动的文件较多,每次都需要添加权限,比较麻烦。
- 解决方案如下,对新建的目录利用setfacl命令添加权限,可以保证该目录下的文件都有修改和上传权限
为文件夹下所有文件及目录设置权限 sudo apt-get update sudo apt-get install acl sudo setfacl -R -m o::rwx <dir路径> # 为其他用户设置权限 # 其他的权限设置 sudo setfacl -R -m u::rwx <dir路径> # 为所有者设置权限 sudo setfacl -R -m g::rwx <dir路径> # 为组设置权限 # 确保新建的目录具有权限 sudo setfacl -d -m u::rwx <dir路径> # 为所有者设置默认权限 sudo setfacl -d -m g::rwx <dir路径> # 为组设置默认权限 sudo setfacl -d -m o::rwx <dir路径> # 为其他用户设置默认权限