Pruner-Zero论文阅读与代码有效复现及相关记录——SparseGPT/Wanda同理

论文: Pruner-Zero: Evolving Symbolic Pruning Metric from scratch for Large Language Models

github地址:https://github.com/pprp/Pruner-Zero

核心idea

  • 自适应求解权重重要性函数方程式

\boldsymbol{S}=f\left( \boldsymbol{W},\boldsymbol{X},\boldsymbol{G} \right)

  • 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
seed0
nsamples128
sparsity_ratio0.5
sparsity_typeunstructured
prune_methodpruner-zero
cache_dirllm_weights
saveout/{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路径>    # 为其他用户设置默认权限
  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值