Análise de código DeepSpeed-Chat e uso detalhado

Neste capítulo, entenderemos como funciona essa ferramenta. Ao entender como ela funciona, podemos:

  • Substitua o modelo pré-treinado e os dados de treinamento: Além do modelo pré-treinado do Facebook, também podemos tentar usar outros modelos; em particular, podemos usar outros dados para otimizar o modelo.
  • Domine o uso do DeepSpeed: No capítulo anterior, sabemos que o DeepSpeed ​​​​é uma poderosa ferramenta de treinamento de modelos em grande escala. Aprenda a usar o DeepSeed, não apenas para treinar LLM, mas também para treinar modelos grandes para outras tarefas de DL.

1. Visão Geral

[Assista ao vídeo explicativo]

DeepSpeed-Chat tem três etapas. Entre eles, as Etapas 1 e 2 são relativamente semelhantes e, em comparação com a Etapa 3, também são relativamente simples. Portanto, aprendemos as funções básicas desta ferramenta estudando o Passo 1. Estudamos principalmente  os programas step1_supervised_finetuning do diretório  e  o código main.py a eles associado  . Interpretaremos o código nos seguintes aspectos:utils

  • Parte de processamento de dados
  • Parte de inicialização do modelo
  • Parte do código de treinamento
  • Compreensão do processamento paralelo DeepSpeed ​​e uso de processos

2 Resumo do código

run_1.3b.sh O processo de treinamento da primeira etapa, Step1, é chamado  pelo script bash  main.py para treinamento, então aprendemos principalmente  main.py o programa.

O script run_1.3b.sh inclui principalmente o seguinte conteúdo

deepspeed main.py \
   --data_path Dahoas/rm-static \
   --data_split 2,4,4 \
   --model_name_or_path facebook/opt-1.3b \
   --per_device_train_batch_size 8 \
   --per_device_eval_batch_size 8 \
   --max_seq_len 512 \
   --learning_rate 9.65e-6 \
   --weight_decay 0.1 \
   --num_train_epochs 2 \
   --gradient_accumulation_steps 1 \
   --lr_scheduler_type cosine \
   --num_warmup_steps 0 \
   --seed 1234 \
   --zero_stage $ZERO_STAGE \
   --deepspeed \
   --output_dir $OUTPUT \
   &> $OUTPUT/training.log

Combinado com  main.py o programa, podemos dividir os parâmetros em três categorias

Parâmetros relacionados aos dados

data_path        : 数据路径,huggingface数据库, 比如:Dahoas/rm-static
data_split       : 数据的拆分方式,比如 2,4,4 是为step1,2,3分配的数据比例
max_seq_len      : 最大序列长度(超过长度会被截掉)
data_output_path : 相关数据的存储地址(local storage,不能是shared storage)

Parâmetros relacionados ao modelo

model_name_or_path : 模型名称或路径,huggingface模型,比如:facebook/opt-1.3b
lora_dim           : 如果大于0,则使用LoRA优化
lora_module_name   : 设置LoRA的范围,比如可以只针对 decoder.layers
only_optimize_lora : 是否只优化LoRA的参数

Parâmetros relacionados ao treinamento

per_device_train_batch_size : 训练时的 Batch size (per device: 每个GPU的Size)
per_device_eval_batch_size  : 评价时的 Batch size (per device)
learning_rate               : 学习率
weight_decay                : 权重衰减,防止模型过拟合的技术。
num_train_epochs            : 训练 epoch 数
gradient_accumulation_steps : 累积多少个 mini-batch 的梯度后再进行一次参数更新。
lr_scheduler_type           : learning rate的调整策略,比如 linear, cosine

velocidade profunda

zero_stage  : 这个对应者DeepSpeed工具中的zero方式,分别是0,1,2,3
offload     : ZeRO-Offload 通过利用主机CPU上的计算和内存资源来执行优化器,从而减少此类模型的GPU计算和内存需求。
local_rank  : 分布式训练时的一个变量,用于标识当前 GPU 设备的本地排名(本机排名,与global-rank不同)
gradient_checkpointing : 降低深度学习模型训练过程中内存消耗的技术

outro

seed        : 随机排序是的seed
output_dir  : 模型的存储目录

3 Conceitos básicos de treinamento distribuído

[Assista ao vídeo explicativo]

args.local_rank
local_rank é uma variável usada durante o treinamento distribuído para identificar a classificação local do dispositivo GPU atual.

Quando args.local_rank for igual a -1, significa que o código não está sendo executado em uma configuração distribuída e usa apenas uma única GPU para treinamento. Se args.local_rank não for igual a -1, significa que o código está sendo executado em uma configuração distribuída e o dispositivo GPU atual recebe uma classificação local exclusiva. O código definirá o dispositivo para a GPU especificada (torch.device("cuda", args.local_rank)) e inicializará o back-end distribuído usando a chamada de função deepspeed.init_distributed().

Nota: Há também uma função do método de inicialização distribuída torch.distributed.init_process_group() no PyTorch. Mas ao usar a biblioteca DeepSpeed, não substitua por deepspeed.init_distributed().

args.global_rank
No treinamento distribuído, cada processo possui uma classificação global exclusiva que identifica a posição do processo no ambiente distribuído. A classificação global varia de 0 a world_size-1, onde world_size é o número total de processos em todo o ambiente distribuído.
Neste programa, global_rank é lido através de torch.distributed.get_rank(). Esta função só pode ser chamada após a inicialização do backend distribuído.

torch.distributed.barrier()
torch.distributed.barrier() é uma função de sincronização usada para sincronizar o estado de cada processo em um ambiente distribuído. Quando esta função for chamada, o processo irá bloquear e esperar até que todos os processos tenham chamado esta função, então ela será desbloqueada e continuará executando o código a seguir.

No treinamento distribuído, torch.distributed.barrier() geralmente é usado para sincronizar as atualizações de gradiente de cada processo. Depois que cada processo completa uma rodada de propagação direta e retropropagação, eles precisam sincronizar seus próprios gradientes e aguardar que outros processos concluam a mesma operação antes de prosseguir para a próxima rodada de atualizações. Neste momento, você pode usar a função torch.distributed.barrier() para obter a sincronização.

Outro uso é que durante o treinamento paralelo dos parâmetros do modelo, a leitura dos dados só precisa ser realizada na GPU com local_rank 0. Outros processos usam torch.distributed.barrier() para bloquear e aguardar a conclusão da leitura dos dados.

3 Código: Dados relacionados

[Assista ao vídeo explicativo]

Ao treinar modelos atuais de PNL, uma prática comum é primeiro converter o texto em Token. Token geralmente se refere a uma palavra ou parte de uma palavra. O Tokenizer será usado ao converter este artigo em Token. O Tokenizer é calculado e treinado nos dados de treinamento. Existem muitas ferramentas para treinar tokenizadores, como a sentença do Google. Um exemplo de token GPT-3 é fornecido abaixo. por exemplo:

  • bom dia: Corresponde a 2 Tokens, cujo ID é [11274, 3329]
  • Bom dia: Correspondentes a 5 Tokens, os IDs são [33768, 102, 41468, 25001, 121]
  • Bom dia: token japonês 6pcs, ID é [2515, 232, 31676, 1792, 230, 29557]
  • 1234567890: Corresponde a 4 Tokens, respectivamente 123, 45, 678, 90
  • xa: Correspondente a 2 Tokens, nomeadamente x e a

Você pode experimentar a divisão de tokens no site da OpenAI: https://platform.openai.com/tokenizer

O tokenizer usado durante o treinamento na ferramenta DS-Chat vem do modelo pré-treinado.Este código usa a classe AutoTokenizer na biblioteca Hugging Face Transformers para instanciar o tokenizer de um modelo pré-treinado. A classe AutoTokenizer pode selecionar e carregar automaticamente o tokenizer correspondente, evitando assim a etapa de seleção manual.

    tokenizer = AutoTokenizer.from_pretrained(args.model_name_or_path, fast_tokenizer=True)
    tokenizer.pad_token = tokenizer.eos_token

A função AutoTokenizer.from_pretrained() possui dois parâmetros obrigatórios, model_name_or_path é o nome ou caminho do modelo pré-treinado, como "bert-base-uncased" ou "/path/to/model/directory". fast_tokenizer: se deve usar o tokenizer rápido. Se for True, será selecionado o tokenizer implementado em Rust, que é mais rápido; caso contrário, será utilizado o tokenizer implementado em Python. O padrão é Verdadeiro.

Função de preparação de dados: create_prompt_dataset

    train_phase = 1
    train_dataset, eval_dataset = create_prompt_dataset(
        args.local_rank, args.data_path, args.data_split,
        args.data_output_path, train_phase, args.seed, tokenizer,
        args.max_seq_len)

O parâmetro local_rank permite que o processamento básico, como download de dados, seja executado apenas na GPU com classificação local 0. Ou seja, os dados são processados ​​apenas uma vez em cada nó. data_output_path precisa ser definido como caminho de armazenamento local, que deve ser usado para armazenar dados locais durante o treinamento distribuído.

Depois, há o amostrador de inicialização. GPU única usa RandomSampler e SequentialSampler, e o processamento distribuído usa DistributedSampler. amostrador é usado principalmente para definir a ordem de adoção dos dados. Por exemplo, a amostragem aleatória pode ser usada para melhorar a robustez do modelo.

    # DataLoaders creation:
    if args.local_rank == -1:
        train_sampler = RandomSampler(train_dataset)
        eval_sampler = SequentialSampler(eval_dataset)
    else:
        train_sampler = DistributedSampler(train_dataset)
        eval_sampler = DistributedSampler(eval_dataset)

A leitura de dados é feita usando o DataLoader padrão do PyTorch. Usando o Dataloader, você pode não apenas definir o amostrador para definir o método de amostragem, mas também executar automaticamente o processamento em lote e suportar o carregamento de dados de vários processos.

    train_dataloader = DataLoader(train_dataset,
                                  collate_fn=default_data_collator,
                                  sampler=train_sampler,
                                  batch_size=args.per_device_train_batch_size)
    eval_dataloader = DataLoader(eval_dataset,
                                 collate_fn=default_data_collator,
                                 sampler=eval_sampler,
                                 batch_size=args.per_device_eval_batch_size)

4 Código: Modelo relacionado

[Assista ao vídeo explicativo]

Inicialização do modelo
O código a seguir é usado para inicializar o modelo.

model = create_hf_model(AutoModelForCausalLM, args.model_name_or_path,
                        tokenizer, ds_config)

Entre eles, AutoModelForCausalLM é uma classe na biblioteca Hugging Face Transformers, que pode selecionar e carregar automaticamente o modelo de Transformer pré-treinado apropriado.Ele suporta uma variedade de modelos de Transformer pré-treinados, incluindo GPT-2, GPT, CTRL, Transformer- XL, XLNet e XLM, etc. Ao usar esta classe, você só precisa especificar o nome ou caminho do modelo para carregar automaticamente o modelo correspondente.

Para código de implementação específico, consulte: utils/model/model_utils.py.

LoRA
Ao definir lora_dim maior que 0, a tecnologia LoRA será usada para ajustar o modelo. Isto reduz significativamente os parâmetros de otimização do modelo e melhora a eficiência da otimização. Em circunstâncias normais, o uso da tecnologia LoRA pode não apenas reduzir o número de parâmetros, mas também melhorar ainda mais o desempenho. Isto ocorre principalmente porque o projeto da rede de gargalo pode evitar o overfitting e, assim, melhorar a robustez do modelo.

    if args.lora_dim > 0:
        model = convert_linear_layer_to_lora(model, args.lora_module_name,
                                             args.lora_dim)
        if args.only_optimize_lora:
            model = only_optimize_lora_parameters(model)

Extraia os parâmetros que precisam ser otimizados otimizador_grouped_parameters

    # Split weights in two groups, one with weight decay and the other not.
    optimizer_grouped_parameters = get_optimizer_grouped_parameters(
        model, args.weight_decay)

    AdamOptimizer = DeepSpeedCPUAdam if args.offload else FusedAdam
    optimizer = AdamOptimizer(optimizer_grouped_parameters,
                              lr=args.learning_rate,
                              betas=(0.9, 0.95))

No código acima, a função get_optimizer_grouped_parameters() é usada para dividir os pesos em dois grupos, um grupo precisa aplicar redução de peso e o outro grupo não. Esta função distingue os parâmetros que precisam aplicar redução de peso dos parâmetros que não o fazem, iterando todos os parâmetros do modelo e verificando se os nomes dos parâmetros contêm strings especiais, como polarização ou LayerNorm.

Explicação dos motivos do agrupamento:  De modo geral, para parâmetros cujos nomes de parâmetros não contêm strings especiais, como polarização ou LayerNorm, consideramos que são parâmetros que precisam aplicar atenuação de peso. Para esses parâmetros, é comum multiplicar suas matrizes de peso pelo hiperparâmetro de redução de peso para reduzir seu peso. Por outro lado, para parâmetros cujos nomes de parâmetros contêm strings especiais, como polarização ou LayerNorm, consideramos que eles são parâmetros que não exigem a aplicação de redução de peso. Isso ocorre porque os parâmetros de polarização ou LayerNorm geralmente são usados ​​apenas para compensar ou dimensionar a saída de outras camadas, em vez dos parâmetros de peso verdadeiro. Ao dividir os pesos em dois grupos e aplicar a redução de peso e não aplicar a redução de peso respectivamente, podemos controlar melhor a complexidade do modelo, melhorando assim o desempenho de generalização do modelo.

Em seguida, defina o otimizador Optimizer.Dependendo dos parâmetros, o otimizador DeepSpeedCPUAdam ou FusedAdam será selecionado. E passou em alguns parâmetros, incluindo parâmetros de agrupamento, taxa de aprendizagem e betas.

Explicação do otimizador Adam:  Na biblioteca Transformers do Hugging Face, existem dois otimizadores Adam para escolher: FusedAdam e DeepSpeedCPUAdam. Todos são otimizadores implementados com base no PyTorch, mas possuem diferentes características de otimização e desempenho em hardwares diferentes. FusedAdam é um otimizador implementado usando a biblioteca NVIDIA Apex, que suporta treinamento de precisão mista e pode calcular gradientes e operações de atualização de peso simultaneamente, melhorando assim a eficiência do treinamento. O otimizador FusedAdam tem melhor desempenho ao usar GPUs NVIDIA que suportam CUDA. DeepSpeedCPUAdam é um otimizador de CPU que faz parte da estrutura DeepSpeed ​​​​e suporta treinamento distribuído e paralelização de modelos. O otimizador DeepSpeedCPUAdam tem melhor desempenho ao usar a CPU. No código acima, se args.offload for True, significa que a otimização baseada em CPU é usada, portanto o otimizador DeepSpeedCPUAdam será selecionado.

Definir lr_scheduler

    num_update_steps_per_epoch = math.ceil(
        len(train_dataloader) / args.gradient_accumulation_steps)
    lr_scheduler = get_scheduler(
        name=args.lr_scheduler_type,
        optimizer=optimizer,
        num_warmup_steps=args.num_warmup_steps,
        num_training_steps=args.num_train_epochs * num_update_steps_per_epoch,
    )

lr_scheduler é usado para planejar como o lr é ajustado durante todo o processo de treinamento. Tipo de agendador lr_scheduler_type, usado para descrever como lr muda, como LinearWarmup, CosineAnnealing, etc. num_warmup_steps O número de etapas de aquecimento especifica o número de etapas no processo de aumento de lr nos estágios iniciais do treinamento. O número total de etapas de treinamento especifica quantas vezes o modelo é atualizado.

Inicialização DS

    model, optimizer, _, lr_scheduler = deepspeed.initialize(
        model=model,
        optimizer=optimizer,
        args=args,
        config=ds_config,
        lr_scheduler=lr_scheduler,
        dist_init_required=True)    

Para otimizar usando DeepSpeed, você precisa usar a função deepspeed.initialize() para inicializar componentes relacionados ao treinamento, como modelo, otimizador e agendador de taxa de aprendizagem. Entre eles, modelo e otimizador são parâmetros obrigatórios, enquanto outros parâmetros são opcionais.

A função deepspeed.initialize() verifica e otimiza os parâmetros passados ​​e retorna componentes como o novo modelo, otimizador e agendador de taxa de aprendizagem. Por exemplo, ele ajusta automaticamente as configurações do otimizador e do acúmulo de gradiente de acordo com as configurações dos parâmetros de treinamento e configuração de hardware e define a estratégia de treinamento distribuído dos pesos do modelo. O parâmetro dist_init_required=True indica se o DeepSpeed ​​​​requer inicialização de treinamento distribuído.

Arquivo de configuração DS
O arquivo de configuração contém informações de configuração relevantes necessárias para o treinamento do modelo DeepSpeed. Você pode ajustar o processo de treinamento modificando-o aqui. Aqui estão as configurações fornecidas em utils/ds_utils.py:

ds_config = {
    "train_batch_size": GLOBAL_BATCH_SIZE,
    "train_micro_batch_size_per_gpu": MICRO_BATCH_SIZE,
    "steps_per_print": 10,
    "zero_optimization": {
        "stage": stage,
        "offload_param": {
            "device": device
        },
        "offload_optimizer": {
            "device": device
        },
        "stage3_param_persistence_threshold": 1e4,
        "stage3_max_live_parameters": 3e7,
        "stage3_prefetch_bucket_size": 3e7,
        "memory_efficient_linear": False
    },
    "fp16": {
        "enabled": True,
        "loss_scale_window": 100
    },
    "gradient_clipping": 1.0,
    "prescale_gradients": False,
    "wall_clock_breakdown": False,
    "hybrid_engine": {
        "enabled": enable_hybrid_engine,
        "inference_tp_size": inference_tp_size,
        "release_inference_cache": release_inference_cache,
        "pin_parameters": pin_parameters,
        "tp_gather_partition_size": tp_gather_partition_size,
    }
}

5 Código: Treinamento

[Assista ao vídeo explicativo]

A seguir está o código de implementação da parte de treinamento. Deve-se notar que após usar o DS, o código da parte de treinamento é diferente do código PyTorch padrão.

    for epoch in range(args.num_train_epochs):
        print_rank_0(
            f"Beginning of Epoch {epoch+1}/{args.num_train_epochs}, Total Micro Batches {len(train_dataloader)}",
            args.global_rank)
        model.train()
        for step, batch in enumerate(train_dataloader):
            batch = to_device(batch, device)
            outputs = model(**batch, use_cache=False)
            loss = outputs.loss
            model.backward(loss)
            model.step()

Quanto **batchà explicação:  esta operação pode passar convenientemente um lote de dados para o modelo, evitar a divisão manual de listas ou tuplas e tornar o código mais conciso e fácil de ler.

*batch significa dividir os elementos em um lote de objetos de lista em parâmetros independentes e passá-los para funções ou métodos. Por exemplo:
batch = (input_ids, attention_mask, labels)
Então, ao usar *batch, na verdade é equivalente a dividir esses objetos Tensor em parâmetros independentes, ou seja:
model(*batch) equivalente a model(input_ids, attention_mask, labels)

**lote significa dividir um lote de objetos de dicionário em parâmetros independentes e passá-los para funções ou métodos. Por exemplo:
batch = {'input_ids': input_ids, 'attention_mask': attention_mask, 'labels': labels}
model(**batch) equivalente a
model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)

6 Código: Avaliação

Esta tarefa avalia o modelo através da perplexidade.

    # Evaluate perplexity on the validation set
    perplexity = evaluation(model, eval_dataloader)

7 Código: Salve o modelo

[Assista ao vídeo explicativo]

    if args.output_dir is not None:
        print_rank_0('saving the final model ...', args.global_rank)
        model = convert_lora_to_linear_layer(model)

        if args.global_rank == 0:
            save_hf_format(model, tokenizer, args)

        if args.zero_stage == 3:
            # For zero stage 3, each gpu only has a part of the model, so we need a special save function
            save_zero_three_model(model,
                                  args.global_rank,
                                  args.output_dir,
                                  zero_stage=args.zero_stage)

8 Resumo do Processo: Usando DeepSpeed ​​​​para ajuste fino do modelo

Com base na análise acima, o processo completo de ajuste fino do modelo é o seguinte:
Parte de dados

  • Leia o tokenizer: leia o tokenizer do modelo pré-treinado
  • Ler dados de processamento train_dataset, eval_dataset
  • Definir train_sampler, eval_sampler
  • Definir train_dataloader, eval_dataloader (usando DataLoader)

peça do modelo

  • Definir parâmetros de configuração do DeepSpeed
  • Importar e instanciar modelo
  • Opcional: configurações LoRA
  • Prepare os parâmetros que precisam ser otimizados: Optimizer_grouped_parameters
  • Definir otimizador
  • Definir lr_scheduler
  • Inicialize deepspeed.initialize

Seção de treinamento e avaliação

  • Comece o treinamento para frente, para trás, atualização de parâmetros
  • avaliar, testar
  • Salvamento de modelo: observe que quando ZeRO é 3, ele precisa ser processado separadamente

problema comum

Q/A 1:  Quando o modelo é inicializado, dschf = HfDeepSpeedConfig(ds_config) é definido e não é chamado posteriormente.
Ao usar zero 3 você precisa definir dschf = HfDeepSpeedConfig(ds_config).
Para obter instruções específicas, consulte: Perguntas e respostas
sobre integração DeepSpeed
​​​​2:  O que é ZeRO?
ZeRO (Zero Redundancy Optimizer) é uma tecnologia de otimização da biblioteca DeepSpeed ​​projetada para melhorar a eficiência e escalabilidade do treinamento de modelos em grande escala. Entre eles, ZeRO Offload é uma variante da tecnologia ZeRO que pode armazenar parâmetros do modelo na CPU, reduzindo assim a ocupação da memória da GPU durante o treinamento do modelo e acelerando operações como acumulação de gradiente, compressão de gradiente e comunicação de parâmetros do modelo. ZeRO 3 é usado ao paralelizar parâmetros de modelo para modelos grandes.

referências

Acho que você gosta

Origin blog.csdn.net/chaishen10000/article/details/131312099
Recomendado
Clasificación